diff --git a/samples/IdentitySample.Mvc/LocalConfig.json b/samples/IdentitySample.Mvc/LocalConfig.json index 06b6876a05..49b1ac00a4 100644 --- a/samples/IdentitySample.Mvc/LocalConfig.json +++ b/samples/IdentitySample.Mvc/LocalConfig.json @@ -3,7 +3,7 @@ "DefaultAdminPassword": "YouShouldChangeThisPassword1!", "Data": { "IdentityConnection": { - "Connectionstring": "Server=(localdb)\\mssqllocaldb;Database=IdentityMvc-11-24-14;Trusted_Connection=True;MultipleActiveResultSets=true" + "Connectionstring": "Server=(localdb)\\mssqllocaldb;Database=IdentityMvc-1-7-15;Trusted_Connection=True;MultipleActiveResultSets=true" } }, "Identity": { diff --git a/src/Microsoft.AspNet.Identity.EntityFramework/RoleStore.cs b/src/Microsoft.AspNet.Identity.EntityFramework/RoleStore.cs index 884b7a5b30..89cf656d5a 100644 --- a/src/Microsoft.AspNet.Identity.EntityFramework/RoleStore.cs +++ b/src/Microsoft.AspNet.Identity.EntityFramework/RoleStore.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; @@ -188,16 +187,39 @@ namespace Microsoft.AspNet.Identity.EntityFramework } /// - /// Find a role by name + /// Find a role by normalized name /// - /// + /// /// /// - public virtual Task FindByNameAsync(string name, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - return Roles.FirstOrDefaultAsync(u => u.Name.ToUpper() == name.ToUpper(), cancellationToken); + return Roles.FirstOrDefaultAsync(r => r.NormalizedName == normalizedName, cancellationToken); + } + + public virtual Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return Task.FromResult(role.NormalizedName); + } + + public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + role.NormalizedName = normalizedName; + return Task.FromResult(0); } private void ThrowIfDisposed() diff --git a/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs b/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs index c21198dcbf..74e6ddb234 100644 --- a/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs +++ b/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs @@ -78,7 +78,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return AutoSaveChanges ? Context.SaveChangesAsync(cancellationToken) : Task.FromResult(0); } - public Task GetUserIdAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task GetUserIdAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -89,7 +89,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(ConvertIdToString(user.Id)); } - public Task GetUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task GetUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -100,7 +100,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(user.UserName); } - public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -112,7 +112,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(0); } - public Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -123,7 +123,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(user.NormalizedUserName); } - public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -420,7 +420,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework private DbSet> UserRoles { get { return Context.Set>(); } } private DbSet> UserLogins { get { return Context.Set>(); } } - public async Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); if (user == null) @@ -431,7 +431,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return await UserClaims.Where(uc => uc.UserId.Equals(user.Id)).Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToListAsync(cancellationToken); } - public async Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); if (user == null) @@ -448,7 +448,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework } } - public async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); if (user == null) @@ -472,7 +472,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework } } - public async Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); if (user == null) @@ -633,19 +633,40 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(user.Email); } + public virtual Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException("user"); + } + return Task.FromResult(user.NormalizedEmail); + } + + public virtual Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException("user"); + } + user.NormalizedEmail = normalizedEmail; + return Task.FromResult(0); + } + /// /// Find an user by email /// /// /// /// - public virtual Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - return Users.FirstOrDefaultAsync(u => u.Email == email, cancellationToken); - // todo: ToUpper blows up with Null Ref - //return Users.FirstOrDefaultAsync(u => u.Email.ToUpper() == email.ToUpper(), cancellationToken); + return Users.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, cancellationToken); } /// @@ -925,7 +946,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework /// /// /// - public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -940,7 +961,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework && userclaims.ClaimType == claim.Type select user; - return (IList)await query.ToListAsync(cancellationToken); + return await query.ToListAsync(cancellationToken); } /// @@ -949,7 +970,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework /// /// /// - public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -967,7 +988,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework where userrole.RoleId.Equals(role.Id) select user; - return (IList) await query.ToListAsync(cancellationToken); + return await query.ToListAsync(cancellationToken); } return new List(); } diff --git a/src/Microsoft.AspNet.Identity/IUserNameNormalizer.cs b/src/Microsoft.AspNet.Identity/ILookupNormalizer.cs similarity index 61% rename from src/Microsoft.AspNet.Identity/IUserNameNormalizer.cs rename to src/Microsoft.AspNet.Identity/ILookupNormalizer.cs index b8d1bfcbf2..159f222ffa 100644 --- a/src/Microsoft.AspNet.Identity/IUserNameNormalizer.cs +++ b/src/Microsoft.AspNet.Identity/ILookupNormalizer.cs @@ -4,15 +4,15 @@ namespace Microsoft.AspNet.Identity { /// - /// Used to normalize a user name + /// Used to normalize keys for consistent lookups /// - public interface IUserNameNormalizer + public interface ILookupNormalizer { /// - /// Returns the normalized user name + /// Returns the normalized key /// - /// + /// /// - string Normalize(string userName); + string Normalize(string key); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IRoleStore.cs b/src/Microsoft.AspNet.Identity/IRoleStore.cs index e4fb97e532..405a47ce31 100644 --- a/src/Microsoft.AspNet.Identity/IRoleStore.cs +++ b/src/Microsoft.AspNet.Identity/IRoleStore.cs @@ -63,6 +63,26 @@ namespace Microsoft.AspNet.Identity Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)); + /// + /// Get a role's normalized name + /// + /// + /// + /// + Task GetNormalizedRoleNameAsync(TRole role, + CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Set a role's normalized name + /// + /// + /// + /// + /// + Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, + CancellationToken cancellationToken = default(CancellationToken)); + + /// /// Finds a role by id /// @@ -72,11 +92,11 @@ namespace Microsoft.AspNet.Identity Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default(CancellationToken)); /// - /// Find a role by name + /// Find a role by normalized name /// - /// + /// /// /// - Task FindByNameAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)); + Task FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserEmailStore.cs b/src/Microsoft.AspNet.Identity/IUserEmailStore.cs index 6569548ffe..816f9383eb 100644 --- a/src/Microsoft.AspNet.Identity/IUserEmailStore.cs +++ b/src/Microsoft.AspNet.Identity/IUserEmailStore.cs @@ -51,9 +51,28 @@ namespace Microsoft.AspNet.Identity /// /// Returns the user associated with this email /// - /// + /// /// /// - Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)); + Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Returns the normalized email + /// + /// + /// + /// + Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Set the normalized email + /// + /// + /// + /// + /// + Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, + CancellationToken cancellationToken = default(CancellationToken)); + } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityErrorDescriber.cs b/src/Microsoft.AspNet.Identity/IdentityErrorDescriber.cs index 9c998bc6a4..39d1677713 100644 --- a/src/Microsoft.AspNet.Identity/IdentityErrorDescriber.cs +++ b/src/Microsoft.AspNet.Identity/IdentityErrorDescriber.cs @@ -24,7 +24,6 @@ namespace Microsoft.AspNet.Identity }; } - public virtual IdentityError PasswordMismatch() { return new IdentityError diff --git a/src/Microsoft.AspNet.Identity/IdentityRole.cs b/src/Microsoft.AspNet.Identity/IdentityRole.cs index 711ff58f32..0127acb84d 100644 --- a/src/Microsoft.AspNet.Identity/IdentityRole.cs +++ b/src/Microsoft.AspNet.Identity/IdentityRole.cs @@ -65,6 +65,7 @@ namespace Microsoft.AspNet.Identity /// Role name /// public virtual string Name { get; set; } + public virtual string NormalizedName { get; set; } /// /// A random value that should change whenever a role is persisted to the store diff --git a/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs index 33b4d5e8f6..6ccc4ce6c5 100644 --- a/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs @@ -57,7 +57,7 @@ namespace Microsoft.Framework.DependencyInjection services.TryAdd(describe.Transient, UserValidator>()); services.TryAdd(describe.Transient, PasswordValidator>()); services.TryAdd(describe.Transient, PasswordHasher>()); - services.TryAdd(describe.Transient()); + services.TryAdd(describe.Transient()); services.TryAdd(describe.Transient, RoleValidator>()); // No interface for the error describer so we can add errors without rev'ing the interface services.TryAdd(describe.Transient()); diff --git a/src/Microsoft.AspNet.Identity/IdentityUser.cs b/src/Microsoft.AspNet.Identity/IdentityUser.cs index 1589199a77..4392bf7db4 100644 --- a/src/Microsoft.AspNet.Identity/IdentityUser.cs +++ b/src/Microsoft.AspNet.Identity/IdentityUser.cs @@ -36,6 +36,8 @@ namespace Microsoft.AspNet.Identity /// public virtual string Email { get; set; } + public virtual string NormalizedEmail { get; set; } + /// /// True if the email is confirmed, default is false /// diff --git a/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs b/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs index e441a7bee0..a52e476f15 100644 --- a/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs +++ b/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Security.Cryptography; -using Microsoft.AspNet.Security.DataProtection; namespace Microsoft.AspNet.Identity { diff --git a/src/Microsoft.AspNet.Identity/RoleManager.cs b/src/Microsoft.AspNet.Identity/RoleManager.cs index cf13cee60a..7b23ca8ab2 100644 --- a/src/Microsoft.AspNet.Identity/RoleManager.cs +++ b/src/Microsoft.AspNet.Identity/RoleManager.cs @@ -25,6 +25,7 @@ namespace Microsoft.AspNet.Identity /// public RoleManager(IRoleStore store, IEnumerable> roleValidators = null, + ILookupNormalizer keyNormalizer = null, IdentityErrorDescriber errors = null) { if (store == null) @@ -32,6 +33,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("store"); } Store = store; + KeyNormalizer = keyNormalizer ?? new UpperInvariantLookupNormalizer(); ErrorDescriber = errors ?? new IdentityErrorDescriber(); if (roleValidators != null) @@ -58,6 +60,11 @@ namespace Microsoft.AspNet.Identity /// public IdentityErrorDescriber ErrorDescriber { get; set; } + /// + /// Used to normalize user names, role names, emails for uniqueness + /// + public ILookupNormalizer KeyNormalizer { get; set; } + /// /// Returns an IQueryable of roles if the store is an IQueryableRoleStore /// @@ -141,9 +148,24 @@ namespace Microsoft.AspNet.Identity { return result; } + await UpdateNormalizedRoleNameAsync(role, cancellationToken); return await Store.CreateAsync(role, cancellationToken); } + /// + /// Update the user's normalized user name + /// + /// + /// + /// + public virtual async Task UpdateNormalizedRoleNameAsync(TRole role, + CancellationToken cancellationToken = default(CancellationToken)) + { + var name = await GetRoleNameAsync(role, cancellationToken); + await Store.SetNormalizedRoleNameAsync(role, NormalizeKey(name), cancellationToken); + } + + /// /// UpdateAsync an existing role /// @@ -164,6 +186,7 @@ namespace Microsoft.AspNet.Identity { return result; } + await UpdateNormalizedRoleNameAsync(role, cancellationToken); return await Store.UpdateAsync(role, cancellationToken); } @@ -199,9 +222,20 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("roleName"); } - return await FindByNameAsync(roleName, cancellationToken) != null; + return await FindByNameAsync(NormalizeKey(roleName), cancellationToken) != null; } + /// + /// Normalize a key (role name) for uniqueness comparisons + /// + /// + /// + public virtual string NormalizeKey(string key) + { + return (KeyNormalizer == null) ? key : KeyNormalizer.Normalize(key); + } + + /// /// FindByLoginAsync a role by id /// @@ -240,6 +274,7 @@ namespace Microsoft.AspNet.Identity { ThrowIfDisposed(); await Store.SetRoleNameAsync(role, name, cancellationToken); + await UpdateNormalizedRoleNameAsync(role, cancellationToken); return IdentityResult.Success; } @@ -271,7 +306,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("roleName"); } - return await Store.FindByNameAsync(roleName, cancellationToken); + return await Store.FindByNameAsync(NormalizeKey(roleName), cancellationToken); } // IRoleClaimStore methods diff --git a/src/Microsoft.AspNet.Identity/SignInManager.cs b/src/Microsoft.AspNet.Identity/SignInManager.cs index 95ca0c1851..fb170b95e7 100644 --- a/src/Microsoft.AspNet.Identity/SignInManager.cs +++ b/src/Microsoft.AspNet.Identity/SignInManager.cs @@ -8,9 +8,9 @@ using System.Security.Claims; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; -using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Identity @@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Identity public class SignInManager where TUser : class { public SignInManager(UserManager userManager, - IContextAccessor contextAccessor, + IHttpContextAccessor contextAccessor, IClaimsIdentityFactory claimsFactory, IOptions optionsAccessor = null) { diff --git a/src/Microsoft.AspNet.Identity/SignInResult.cs b/src/Microsoft.AspNet.Identity/SignInResult.cs index e125690fe7..9c88817de2 100644 --- a/src/Microsoft.AspNet.Identity/SignInResult.cs +++ b/src/Microsoft.AspNet.Identity/SignInResult.cs @@ -78,7 +78,5 @@ namespace Microsoft.AspNet.Identity { get { return _twoFactorRequired; } } - - } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UpperInvariantUserNameNormalizer.cs b/src/Microsoft.AspNet.Identity/UpperInvariantLookupNormalizer.cs similarity index 54% rename from src/Microsoft.AspNet.Identity/UpperInvariantUserNameNormalizer.cs rename to src/Microsoft.AspNet.Identity/UpperInvariantLookupNormalizer.cs index 1564ecd3d4..9bfa9454a3 100644 --- a/src/Microsoft.AspNet.Identity/UpperInvariantUserNameNormalizer.cs +++ b/src/Microsoft.AspNet.Identity/UpperInvariantLookupNormalizer.cs @@ -6,22 +6,22 @@ using System; namespace Microsoft.AspNet.Identity { /// - /// Normalizes user names via ToUpperInvariant() + /// Normalizes via ToUpperInvariant() /// - public class UpperInvariantUserNameNormalizer : IUserNameNormalizer + public class UpperInvariantLookupNormalizer : ILookupNormalizer { /// - /// Normalizes user names via ToUpperInvariant() + /// Normalizes via ToUpperInvariant() /// - /// + /// /// - public string Normalize(string userName) + public string Normalize(string key) { - if (userName == null) + if (key == null) { return null; } - return userName.Normalize().ToUpperInvariant(); + return key.Normalize().ToUpperInvariant(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UserManager.cs b/src/Microsoft.AspNet.Identity/UserManager.cs index d57b654505..1f8f4b663a 100644 --- a/src/Microsoft.AspNet.Identity/UserManager.cs +++ b/src/Microsoft.AspNet.Identity/UserManager.cs @@ -30,20 +30,23 @@ namespace Microsoft.AspNet.Identity private IdentityOptions _options; /// - /// Constructor which takes a service provider and user store + /// Constructor /// /// /// /// - /// - /// - /// + /// + /// + /// + /// + /// + /// public UserManager(IUserStore store, IOptions optionsAccessor = null, IPasswordHasher passwordHasher = null, IEnumerable> userValidators = null, IEnumerable> passwordValidators = null, - IUserNameNormalizer userNameNormalizer = null, + ILookupNormalizer keyNormalizer = null, IdentityErrorDescriber errors = null, IEnumerable> tokenProviders = null, IEnumerable msgProviders = null) @@ -55,7 +58,7 @@ namespace Microsoft.AspNet.Identity Store = store; Options = optionsAccessor?.Options ?? new IdentityOptions(); PasswordHasher = passwordHasher ?? new PasswordHasher(); - UserNameNormalizer = userNameNormalizer ?? new UpperInvariantUserNameNormalizer(); + KeyNormalizer = keyNormalizer ?? new UpperInvariantLookupNormalizer(); ErrorDescriber = errors ?? new IdentityErrorDescriber(); if (userValidators != null) { @@ -126,9 +129,9 @@ namespace Microsoft.AspNet.Identity public IList> PasswordValidators { get; } = new List>(); /// - /// Used to normalize user names for uniqueness + /// Used to normalize user names and emails for uniqueness /// - public IUserNameNormalizer UserNameNormalizer { get; set; } + public ILookupNormalizer KeyNormalizer { get; set; } /// /// Used to generate public API error messages @@ -353,6 +356,7 @@ namespace Microsoft.AspNet.Identity await GetUserLockoutStore().SetLockoutEnabledAsync(user, true, cancellationToken); } await UpdateNormalizedUserNameAsync(user, cancellationToken); + await UpdateNormalizedEmailAsync(user, cancellationToken); return await Store.CreateAsync(user, cancellationToken); } @@ -376,6 +380,7 @@ namespace Microsoft.AspNet.Identity return result; } await UpdateNormalizedUserNameAsync(user, cancellationToken); + await UpdateNormalizedEmailAsync(user, cancellationToken); return await Store.UpdateAsync(user, cancellationToken); } @@ -423,7 +428,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("userName"); } - userName = NormalizeUserName(userName); + userName = NormalizeKey(userName); return Store.FindByNameAsync(userName, cancellationToken); } @@ -467,13 +472,13 @@ namespace Microsoft.AspNet.Identity } /// - /// Normalize a user name for uniqueness comparisons + /// Normalize a key (user name, email) for uniqueness comparisons /// /// /// - public virtual string NormalizeUserName(string userName) + public virtual string NormalizeKey(string key) { - return (UserNameNormalizer == null) ? userName : UserNameNormalizer.Normalize(userName); + return (KeyNormalizer == null) ? key : KeyNormalizer.Normalize(key); } /// @@ -485,8 +490,8 @@ namespace Microsoft.AspNet.Identity public virtual async Task UpdateNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { - var userName = await GetUserNameAsync(user, cancellationToken); - await Store.SetNormalizedUserNameAsync(user, NormalizeUserName(userName), cancellationToken); + var normalizedName = NormalizeKey(await GetUserNameAsync(user, cancellationToken)); + await Store.SetNormalizedUserNameAsync(user, normalizedName, cancellationToken); } /// @@ -1215,10 +1220,10 @@ namespace Microsoft.AspNet.Identity } // IUserEmailStore methods - internal IUserEmailStore GetEmailStore() + internal IUserEmailStore GetEmailStore(bool throwOnFail = true) { var cast = Store as IUserEmailStore; - if (cast == null) + if (throwOnFail && cast == null) { throw new NotSupportedException(Resources.StoreNotIUserEmailStore); } @@ -1281,9 +1286,27 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("email"); } - return store.FindByEmailAsync(email, cancellationToken); + return store.FindByEmailAsync(NormalizeKey(email), cancellationToken); } + /// + /// Update the user's normalized email + /// + /// + /// + /// + public virtual async Task UpdateNormalizedEmailAsync(TUser user, + CancellationToken cancellationToken = default(CancellationToken)) + { + var store = GetEmailStore(throwOnFail: false); + if (store != null) + { + var email = await GetEmailAsync(user, cancellationToken); + await store.SetNormalizedEmailAsync(user, NormalizeKey(email), cancellationToken); + } + } + + /// /// Get the confirmation token for the user /// diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs index 2efc785d71..9299a59ceb 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs @@ -76,23 +76,10 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test var role = new IdentityRole("UpdateRoleName"); IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); Assert.Null(await manager.FindByNameAsync("New")); - role.Name = "New"; + IdentityResultAssert.IsSuccess(await manager.SetRoleNameAsync(role, "New")); IdentityResultAssert.IsSuccess(await manager.UpdateAsync(role)); Assert.NotNull(await manager.FindByNameAsync("New")); Assert.Null(await manager.FindByNameAsync("UpdateAsync")); } - - [Fact] - public async Task CanSetUserName() - { - var manager = TestIdentityFactory.CreateRoleManager(); - var role = new IdentityRole("UpdateRoleName"); - IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); - Assert.Null(await manager.FindByNameAsync("New")); - IdentityResultAssert.IsSuccess(await manager.SetRoleNameAsync(role, "New")); - Assert.NotNull(await manager.FindByNameAsync("New")); - Assert.Null(await manager.FindByNameAsync("UpdateAsync")); - } - } } diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs index 81495de872..9f922d9a3b 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs @@ -4,6 +4,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Identity.Test; @@ -30,7 +31,7 @@ namespace Microsoft.AspNet.Identity.InMemory.Test var response = new Mock(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == isPersistent), It.IsAny())).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); app.UseServices(services => { diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryRoleStore.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryRoleStore.cs index 352311b56c..0f250c4615 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryRoleStore.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryRoleStore.cs @@ -97,6 +97,17 @@ namespace Microsoft.AspNet.Identity.InMemory return Task.FromResult(0); } + public Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(role.NormalizedName); + } + + public Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + role.NormalizedName = normalizedName; + return Task.FromResult(0); + } + public IQueryable Roles { get { return _roles.Values.AsQueryable(); } diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs index 8d04d1ae4a..ab3521f6b4 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs @@ -108,6 +108,18 @@ namespace Microsoft.AspNet.Identity.InMemory return Task.FromResult(user.Email); } + public Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(user.NormalizedEmail); + } + + public Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + user.NormalizedEmail = normalizedEmail; + return Task.FromResult(0); + } + + public Task GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult(user.EmailConfirmed); @@ -123,7 +135,7 @@ namespace Microsoft.AspNet.Identity.InMemory { return Task.FromResult( - Users.FirstOrDefault(u => String.Equals(u.Email, email, StringComparison.OrdinalIgnoreCase))); + Users.FirstOrDefault(u => u.NormalizedEmail == email)); } public Task GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) @@ -262,7 +274,7 @@ namespace Microsoft.AspNet.Identity.InMemory { return Task.FromResult( - Users.FirstOrDefault(u => String.Equals(u.UserName, userName, StringComparison.OrdinalIgnoreCase))); + Users.FirstOrDefault(u => u.NormalizedUserName == userName)); } public Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) diff --git a/test/Microsoft.AspNet.Identity.Test/IHttpContextAccessor.cs b/test/Microsoft.AspNet.Identity.Test/IHttpContextAccessor.cs new file mode 100644 index 0000000000..b812ed6e8b --- /dev/null +++ b/test/Microsoft.AspNet.Identity.Test/IHttpContextAccessor.cs @@ -0,0 +1,6 @@ +namespace Microsoft.AspNet.Identity.Test +{ + internal interface IHttpContextAccessor + { + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs b/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs index 01c7f8281e..488fcc7545 100644 --- a/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs @@ -148,6 +148,11 @@ namespace Microsoft.AspNet.Identity.Test throw new NotImplementedException(); } + public Task GetNormalizedRoleNameAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); @@ -173,6 +178,11 @@ namespace Microsoft.AspNet.Identity.Test throw new NotImplementedException(); } + public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); diff --git a/test/Microsoft.AspNet.Identity.Test/NoopRoleStore.cs b/test/Microsoft.AspNet.Identity.Test/NoopRoleStore.cs index a3c576ca51..4d62b972bf 100644 --- a/test/Microsoft.AspNet.Identity.Test/NoopRoleStore.cs +++ b/test/Microsoft.AspNet.Identity.Test/NoopRoleStore.cs @@ -51,5 +51,15 @@ namespace Microsoft.AspNet.Identity.Test { return Task.FromResult(null); } + + public Task GetNormalizedRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task SetNormalizedRoleNameAsync(TestRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/RoleManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/RoleManagerTest.cs index d57c4f2c25..cab11a83df 100644 --- a/test/Microsoft.AspNet.Identity.Test/RoleManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/RoleManagerTest.cs @@ -6,12 +6,51 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Moq; using Xunit; namespace Microsoft.AspNet.Identity.Test { public class RoleManagerTest { + [Fact] + public async Task CreateCallsStore() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.CreateAsync(role, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetRoleNameAsync(role, CancellationToken.None)).Returns(Task.FromResult(role.Name)).Verifiable(); + store.Setup(s => s.SetNormalizedRoleNameAsync(role, role.Name.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + var roleManager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await roleManager.CreateAsync(role); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateCallsStore() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.UpdateAsync(role, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetRoleNameAsync(role, CancellationToken.None)).Returns(Task.FromResult(role.Name)).Verifiable(); + store.Setup(s => s.SetNormalizedRoleNameAsync(role, role.Name.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + var roleManager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await roleManager.UpdateAsync(role); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + [Fact] public void RolesQueryableFailWhenStoreNotImplemented() { @@ -20,6 +59,59 @@ namespace Microsoft.AspNet.Identity.Test Assert.Throws(() => manager.Roles.Count()); } + [Fact] + public async Task FindByNameCallsStoreWithNormalizedName() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.FindByNameAsync("FOO", CancellationToken.None)).Returns(Task.FromResult(role)).Verifiable(); + var manager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await manager.FindByNameAsync(role.Name); + + // Assert + Assert.Equal(role, result); + store.VerifyAll(); + } + + [Fact] + public async Task CanFindByNameCallsStoreWithoutNormalizedName() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.FindByNameAsync(role.Name, CancellationToken.None)).Returns(Task.FromResult(role)).Verifiable(); + var manager = MockHelpers.TestRoleManager(store.Object); + manager.KeyNormalizer = null; + + // Act + var result = await manager.FindByNameAsync(role.Name); + + // Assert + Assert.Equal(role, result); + store.VerifyAll(); + } + + [Fact] + public async Task RoleExistsCallsStoreWithNormalizedName() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.FindByNameAsync("FOO", CancellationToken.None)).Returns(Task.FromResult(role)).Verifiable(); + var manager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await manager.RoleExistsAsync(role.Name); + + // Assert + Assert.True(result); + store.VerifyAll(); + } + + [Fact] public void DisposeAfterDisposeDoesNotThrow() { @@ -107,6 +199,16 @@ namespace Microsoft.AspNet.Identity.Test { throw new NotImplementedException(); } + + public Task GetNormalizedRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedRoleNameAsync(TestRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs b/test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs index 5ae623e01d..1528665dde 100644 --- a/test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs @@ -5,6 +5,7 @@ using System; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Security; @@ -43,7 +44,7 @@ namespace Microsoft.AspNet.Identity.Test var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); var httpContext = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, contextAccessor.Object, claimsManager.Object, options.Object); @@ -78,7 +79,7 @@ namespace Microsoft.AspNet.Identity.Test var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); var httpContext = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, contextAccessor.Object, claimsManager.Object, options.Object); @@ -112,7 +113,7 @@ namespace Microsoft.AspNet.Identity.Test var identityOptions = new IdentityOptions { SecurityStampValidationInterval = TimeSpan.Zero }; var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, contextAccessor.Object, claimsManager.Object, options.Object); @@ -146,7 +147,7 @@ namespace Microsoft.AspNet.Identity.Test var identityOptions = new IdentityOptions { SecurityStampValidationInterval = TimeSpan.FromDays(1) }; var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, contextAccessor.Object, claimsManager.Object, options.Object); diff --git a/test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs index 60876733cc..7d1d1d6c90 100644 --- a/test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs @@ -7,6 +7,7 @@ using System.Security.Claims; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; using Microsoft.Framework.DependencyInjection; @@ -31,7 +32,7 @@ namespace Microsoft.AspNet.Identity.Test // TODO: how to functionally test context? // var context = new DefaultHttpContext(new FeatureCollection()); - // var contextAccessor = new Mock>(); + // var contextAccessor = new Mock(); // contextAccessor.Setup(a => a.Value).Returns(context); // app.UseServices(services => // { @@ -70,7 +71,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.Throws("userManager", () => new SignInManager(null, null, null, null)); var userManager = MockHelpers.MockUserManager().Object; Assert.Throws("contextAccessor", () => new SignInManager(userManager, null, null, null)); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); Assert.Throws("contextAccessor", () => new SignInManager(userManager, contextAccessor.Object, null, null)); var context = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); @@ -93,7 +94,7 @@ namespace Microsoft.AspNet.Identity.Test // var response = new Mock(); // context.Setup(c => c.Response).Returns(response.Object).Verifiable(); // response.Setup(r => r.SignIn(testIdentity, It.IsAny())).Verifiable(); - // var contextAccessor = new Mock>(); + // var contextAccessor = new Mock(); // contextAccessor.Setup(a => a.Value).Returns(context.Object); // var helper = new HttpAuthenticationManager(contextAccessor.Object); @@ -117,7 +118,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.IsLockedOutAsync(user, CancellationToken.None)).ReturnsAsync(true).Verifiable(); manager.Setup(m => m.FindByNameAsync(user.UserName, CancellationToken.None)).ReturnsAsync(user).Verifiable(); var context = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -152,7 +153,7 @@ namespace Microsoft.AspNet.Identity.Test var response = new Mock(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == isPersistent), It.IsAny())).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -189,7 +190,7 @@ namespace Microsoft.AspNet.Identity.Test var response = new Mock(); response.Setup(r => r.SignIn(It.IsAny(), It.IsAny())).Verifiable(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -238,7 +239,7 @@ namespace Microsoft.AspNet.Identity.Test var response = new Mock(); response.Setup(r => r.SignIn(It.Is(id => id.Name == user.Id))).Verifiable(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -282,7 +283,7 @@ namespace Microsoft.AspNet.Identity.Test response.Setup(r => r.SignIn( It.Is(v => v.IsPersistent == isPersistent), It.Is(i => i.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider))).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -342,7 +343,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.GetUserNameAsync(user, CancellationToken.None)).ReturnsAsync(user.UserName).Verifiable(); var context = new Mock(); var response = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); var twoFactorInfo = new SignInManager.TwoFactorAuthenticationInfo { UserId = user.Id }; var loginProvider = "loginprovider"; var id = SignInManager.StoreTwoFactorInfo(user.Id, externalLogin ? loginProvider : null); @@ -397,7 +398,7 @@ namespace Microsoft.AspNet.Identity.Test var manager = MockHelpers.MockUserManager(); var context = new Mock(); var response = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); var options = new Mock>(); @@ -452,7 +453,7 @@ namespace Microsoft.AspNet.Identity.Test id.AddClaim(new Claim(ClaimTypes.Name, user.Id)); var authResult = new AuthenticationResult(id, new AuthenticationProperties(), new AuthenticationDescription()); context.Setup(c => c.AuthenticateAsync(IdentityOptions.TwoFactorRememberMeCookieAuthenticationType)).ReturnsAsync(authResult).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -487,7 +488,7 @@ namespace Microsoft.AspNet.Identity.Test response.Setup(r => r.SignOut(authenticationType)).Verifiable(); response.Setup(r => r.SignOut(IdentityOptions.TwoFactorUserIdCookieAuthenticationType)).Verifiable(); response.Setup(r => r.SignOut(IdentityOptions.ExternalCookieAuthenticationType)).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -518,7 +519,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.FindByNameAsync(user.UserName, CancellationToken.None)).ReturnsAsync(user).Verifiable(); manager.Setup(m => m.CheckPasswordAsync(user, "bogus", CancellationToken.None)).ReturnsAsync(false).Verifiable(); var context = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -543,7 +544,7 @@ namespace Microsoft.AspNet.Identity.Test var manager = MockHelpers.MockUserManager(); manager.Setup(m => m.FindByNameAsync("bogus", CancellationToken.None)).ReturnsAsync(null).Verifiable(); var context = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -579,7 +580,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.FindByNameAsync(user.UserName, CancellationToken.None)).ReturnsAsync(user).Verifiable(); manager.Setup(m => m.CheckPasswordAsync(user, "bogus", CancellationToken.None)).ReturnsAsync(false).Verifiable(); var context = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -618,7 +619,7 @@ namespace Microsoft.AspNet.Identity.Test context.Setup(c => c.Response).Returns(response.Object).Verifiable(); response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == false), It.IsAny())).Verifiable(); } - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -659,7 +660,7 @@ namespace Microsoft.AspNet.Identity.Test response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == false), It.IsAny())).Verifiable(); } - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); diff --git a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs index cb3530fb25..b340caa63f 100644 --- a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs @@ -43,6 +43,29 @@ namespace Microsoft.AspNet.Identity.Test var store = new Mock>(); var user = new TestUser { UserName = "Foo" }; store.Setup(s => s.CreateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.CreateAsync(user); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task CreateCallsUpdateEmailStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo", Email = "Foo@foo.com" }; + store.Setup(s => s.CreateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.GetEmailAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.Email)).Verifiable(); + store.Setup(s => s.SetNormalizedEmailAsync(user, user.Email.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); var userManager = MockHelpers.TestUserManager(store.Object); // Act @@ -76,6 +99,29 @@ namespace Microsoft.AspNet.Identity.Test // Setup var store = new Mock>(); var user = new TestUser { UserName = "Foo" }; + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.UpdateAsync(user); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateWillUpdateNormalizedEmail() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo", Email = "email" }; + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.GetEmailAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.Email)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.SetNormalizedEmailAsync(user, user.Email.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); var userManager = MockHelpers.TestUserManager(store.Object); @@ -93,7 +139,9 @@ namespace Microsoft.AspNet.Identity.Test // Setup var store = new Mock>(); var user = new TestUser(); - store.Setup(s => s.SetUserNameAsync(user, It.IsAny(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.SetUserNameAsync(user, "foo", CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult("foo")).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, "FOO", CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); var userManager = MockHelpers.TestUserManager(store.Object); @@ -146,8 +194,8 @@ namespace Microsoft.AspNet.Identity.Test var store = new Mock>(); var user = new TestUser {UserName="Foo"}; store.Setup(s => s.FindByNameAsync(user.UserName, CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); - var userManager = MockHelpers.TestUserManager(store.Object); - userManager.UserNameNormalizer = null; + var userManager = MockHelpers.TestUserManager(store.Object); + userManager.KeyNormalizer = null; // Act var result = await userManager.FindByNameAsync(user.UserName); @@ -157,6 +205,41 @@ namespace Microsoft.AspNet.Identity.Test store.VerifyAll(); } + [Fact] + public async Task FindByEmailCallsStoreWithNormalizedEmail() + { + // Setup + var store = new Mock>(); + var user = new TestUser { Email = "Foo" }; + store.Setup(s => s.FindByEmailAsync(user.Email.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.FindByEmailAsync(user.Email); + + // Assert + Assert.Equal(user, result); + store.VerifyAll(); + } + + [Fact] + public async Task CanFindByEmailCallsStoreWithoutNormalizedEmail() + { + // Setup + var store = new Mock>(); + var user = new TestUser { Email = "Foo" }; + store.Setup(s => s.FindByEmailAsync(user.Email, CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + userManager.KeyNormalizer = null; + + // Act + var result = await userManager.FindByEmailAsync(user.Email); + + // Assert + Assert.Equal(user, result); + store.VerifyAll(); + } + [Fact] public async Task AddToRolesCallsStore() { @@ -961,6 +1044,16 @@ namespace Microsoft.AspNet.Identity.Test { return Task.FromResult>(new List()); } + + public Task GetNormalizedEmailAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(""); + } + + public Task SetNormalizedEmailAsync(TestUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } } private class NoOpTokenProvider : IUserTokenProvider @@ -1228,6 +1321,16 @@ namespace Microsoft.AspNet.Identity.Test { throw new NotImplementedException(); } + + public Task GetNormalizedEmailAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedEmailAsync(TestUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } } [Fact] @@ -1245,7 +1348,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Options.User.RequireUniqueEmail = true; var user = new TestUser() { UserName = "dupeEmail", Email = "dupe@email.com" }; var user2 = new TestUser() { UserName = "dupeEmail2", Email = "dupe@email.com" }; - store.Setup(s => s.FindByEmailAsync(user.Email, CancellationToken.None)) + store.Setup(s => s.FindByEmailAsync("DUPE@EMAIL.COM", CancellationToken.None)) .Returns(Task.FromResult(user2)) .Verifiable(); store.Setup(s => s.GetUserIdAsync(user2, CancellationToken.None)) diff --git a/test/Shared/MockHelpers.cs b/test/Shared/MockHelpers.cs index 4a73bba084..f70541bec0 100644 --- a/test/Shared/MockHelpers.cs +++ b/test/Shared/MockHelpers.cs @@ -20,21 +20,17 @@ namespace Microsoft.AspNet.Identity.Test return mgr; } - public static Mock> MockRoleManager() where TRole : class + public static Mock> MockRoleManager(IRoleStore store = null) where TRole : class { - var store = new Mock>(); + store = store ?? new Mock>().Object; var roles = new List>(); roles.Add(new RoleValidator()); - return new Mock>(store.Object, roles, null); + return new Mock>(store, roles, null, null); } - public static UserManager TestUserManager() where TUser : class - { - return TestUserManager(new Mock>().Object); - } - - public static UserManager TestUserManager(IUserStore store) where TUser : class + public static UserManager TestUserManager(IUserStore store = null) where TUser : class { + store = store ?? new Mock>().Object; var validator = new Mock>(); var userManager = new UserManager(store); userManager.UserValidators.Add(validator.Object); @@ -43,5 +39,14 @@ namespace Microsoft.AspNet.Identity.Test .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); return userManager; } + + public static RoleManager TestRoleManager(IRoleStore store = null) where TRole : class + { + store = store ?? new Mock>().Object; + var roles = new List>(); + roles.Add(new RoleValidator()); + return new RoleManager(store, roles); + } + } } \ No newline at end of file diff --git a/test/Shared/UserManagerTestBase.cs b/test/Shared/UserManagerTestBase.cs index 2b27a389b1..1be1741852 100644 --- a/test/Shared/UserManagerTestBase.cs +++ b/test/Shared/UserManagerTestBase.cs @@ -1016,10 +1016,10 @@ namespace Microsoft.AspNet.Identity.Test Assert.False(await manager.RoleExistsAsync(role.Name)); IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); Assert.True(await manager.RoleExistsAsync(role.Name)); - role.Name = "Changed"; + IdentityResultAssert.IsSuccess(await manager.SetRoleNameAsync(role, "Changed")); IdentityResultAssert.IsSuccess(await manager.UpdateAsync(role)); Assert.False(await manager.RoleExistsAsync("update")); - Assert.Equal(role, await manager.FindByNameAsync(role.Name)); + Assert.Equal(role, await manager.FindByNameAsync("Changed")); } [Fact]