From b13d26cab619c382f5681023559bbe35897bc2c2 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Mon, 3 Mar 2014 17:25:39 -0800 Subject: [PATCH] Flesh out implementation, add InMemoryTests - Implement RoleManager - Replace IIdentityValidator with IUser/Role/Password Validator - Add test project.json and working tests for InMemoryUserStore --- Identity.sln | 17 +- .../InMemoryUser.cs | 9 +- .../InMemoryUserStore.cs | 110 +-- .../IIdentityValidator.cs | 18 - .../IPasswordValidator.cs | 16 + .../IRoleValidator.cs | 18 + .../IUserValidator.cs | 18 + .../PasswordValidator.cs | 118 +++ src/Microsoft.AspNet.Identity/RoleManager.cs | 213 ++++++ .../RoleValidator.cs | 68 ++ src/Microsoft.AspNet.Identity/UserManager.cs | 61 +- .../UserValidator.cs | 115 +++ .../InMemoryUserStoreTest.cs | 691 ++++++++++++++++++ .../UnitTestHelper.cs | 29 + .../project.json | 15 + .../project.json | 14 + 16 files changed, 1407 insertions(+), 123 deletions(-) delete mode 100644 src/Microsoft.AspNet.Identity/IIdentityValidator.cs create mode 100644 src/Microsoft.AspNet.Identity/IPasswordValidator.cs create mode 100644 src/Microsoft.AspNet.Identity/IRoleValidator.cs create mode 100644 src/Microsoft.AspNet.Identity/IUserValidator.cs create mode 100644 src/Microsoft.AspNet.Identity/PasswordValidator.cs create mode 100644 src/Microsoft.AspNet.Identity/RoleManager.cs create mode 100644 src/Microsoft.AspNet.Identity/RoleValidator.cs create mode 100644 src/Microsoft.AspNet.Identity/UserValidator.cs create mode 100644 test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStoreTest.cs create mode 100644 test/Microsoft.AspNet.Identity.InMemory.Test/UnitTestHelper.cs create mode 100644 test/Microsoft.AspNet.Identity.InMemory.Test/project.json create mode 100644 test/Microsoft.AspNet.Identity.Test/project.json diff --git a/Identity.sln b/Identity.sln index 6b25553d56..c754561c3a 100644 --- a/Identity.sln +++ b/Identity.sln @@ -23,6 +23,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Identity.I EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Identity.InMemory.k10", "src\Microsoft.AspNet.Identity.InMemory\Microsoft.AspNet.Identity.InMemory.k10.csproj", "{D2E7A146-C39F-4302-8EA3-BFA8C1082939}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Identity.Test.net45", "test\Microsoft.AspNet.Identity.Test\Microsoft.AspNet.Identity.Test.net45.csproj", "{E00E23B0-79B8-41E1-9998-57FECA1F2535}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Aspnet.Identity.InMemory.Test.net45", "test\Microsoft.Aspnet.Identity.InMemory.Test\Microsoft.Aspnet.Identity.InMemory.Test.net45.csproj", "{9022EBC9-BAD4-4BCB-85F5-2BB0DD155825}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,7 +38,6 @@ Global {B72401D7-47F6-4A98-89D5-CCBFEFC5B2B8}.Release|Any CPU.ActiveCfg = Release|Any CPU {B72401D7-47F6-4A98-89D5-CCBFEFC5B2B8}.Release|Any CPU.Build.0 = Release|Any CPU {6211450F-FFB8-431F-84E2-9A7620875260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6211450F-FFB8-431F-84E2-9A7620875260}.Debug|Any CPU.Build.0 = Debug|Any CPU {6211450F-FFB8-431F-84E2-9A7620875260}.Release|Any CPU.ActiveCfg = Release|Any CPU {6211450F-FFB8-431F-84E2-9A7620875260}.Release|Any CPU.Build.0 = Release|Any CPU {E52361C9-1F0B-4229-86A0-E5C7C12A5429}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -42,7 +45,6 @@ Global {E52361C9-1F0B-4229-86A0-E5C7C12A5429}.Release|Any CPU.ActiveCfg = Release|Any CPU {E52361C9-1F0B-4229-86A0-E5C7C12A5429}.Release|Any CPU.Build.0 = Release|Any CPU {D32483A4-B617-480C-81E6-49CD596B9A34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D32483A4-B617-480C-81E6-49CD596B9A34}.Debug|Any CPU.Build.0 = Debug|Any CPU {D32483A4-B617-480C-81E6-49CD596B9A34}.Release|Any CPU.ActiveCfg = Release|Any CPU {D32483A4-B617-480C-81E6-49CD596B9A34}.Release|Any CPU.Build.0 = Release|Any CPU {054B3FFA-7196-466F-9A8A-593FFE037A69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -50,9 +52,16 @@ Global {054B3FFA-7196-466F-9A8A-593FFE037A69}.Release|Any CPU.ActiveCfg = Release|Any CPU {054B3FFA-7196-466F-9A8A-593FFE037A69}.Release|Any CPU.Build.0 = Release|Any CPU {D2E7A146-C39F-4302-8EA3-BFA8C1082939}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D2E7A146-C39F-4302-8EA3-BFA8C1082939}.Debug|Any CPU.Build.0 = Debug|Any CPU {D2E7A146-C39F-4302-8EA3-BFA8C1082939}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2E7A146-C39F-4302-8EA3-BFA8C1082939}.Release|Any CPU.Build.0 = Release|Any CPU + {E00E23B0-79B8-41E1-9998-57FECA1F2535}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E00E23B0-79B8-41E1-9998-57FECA1F2535}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E00E23B0-79B8-41E1-9998-57FECA1F2535}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E00E23B0-79B8-41E1-9998-57FECA1F2535}.Release|Any CPU.Build.0 = Release|Any CPU + {9022EBC9-BAD4-4BCB-85F5-2BB0DD155825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9022EBC9-BAD4-4BCB-85F5-2BB0DD155825}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9022EBC9-BAD4-4BCB-85F5-2BB0DD155825}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9022EBC9-BAD4-4BCB-85F5-2BB0DD155825}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -60,6 +69,8 @@ Global GlobalSection(NestedProjects) = preSolution {F6B0C0E9-C346-49D0-B583-95B6CE04BB1B} = {0F647068-6602-4E24-B1DC-8ED91481A50A} {77CEDA6C-A833-455D-8357-649BFD944724} = {0F647068-6602-4E24-B1DC-8ED91481A50A} + {E00E23B0-79B8-41E1-9998-57FECA1F2535} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} + {9022EBC9-BAD4-4BCB-85F5-2BB0DD155825} = {52D59F18-62D2-4D17-8CF2-BE192445AF8E} {B72401D7-47F6-4A98-89D5-CCBFEFC5B2B8} = {F6B0C0E9-C346-49D0-B583-95B6CE04BB1B} {E52361C9-1F0B-4229-86A0-E5C7C12A5429} = {F6B0C0E9-C346-49D0-B583-95B6CE04BB1B} {054B3FFA-7196-466F-9A8A-593FFE037A69} = {F6B0C0E9-C346-49D0-B583-95B6CE04BB1B} diff --git a/src/Microsoft.AspNet.Identity.InMemory/InMemoryUser.cs b/src/Microsoft.AspNet.Identity.InMemory/InMemoryUser.cs index b90af5346c..ae445848d7 100644 --- a/src/Microsoft.AspNet.Identity.InMemory/InMemoryUser.cs +++ b/src/Microsoft.AspNet.Identity.InMemory/InMemoryUser.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.Linq; #if NET45 using System.Security.Claims; #else using System.Security.ClaimsK; #endif -using System.Threading.Tasks; -using Microsoft.AspNet.Identity; namespace Microsoft.AspNet.Identity.InMemory { @@ -17,12 +14,16 @@ namespace Microsoft.AspNet.Identity.InMemory private readonly IList _logins; private readonly IList _roles; - public InMemoryUser(string name) + public InMemoryUser() { Id = Guid.NewGuid().ToString(); _logins = new List(); _claims = new List(); _roles = new List(); + } + + public InMemoryUser(string name) : this() + { UserName = name; } diff --git a/src/Microsoft.AspNet.Identity.InMemory/InMemoryUserStore.cs b/src/Microsoft.AspNet.Identity.InMemory/InMemoryUserStore.cs index 9385246b7a..4193b062da 100644 --- a/src/Microsoft.AspNet.Identity.InMemory/InMemoryUserStore.cs +++ b/src/Microsoft.AspNet.Identity.InMemory/InMemoryUserStore.cs @@ -10,55 +10,57 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Identity.InMemory { - public class InMemoryUserStore : - IUserStore, - IUserLoginStore, - IUserRoleStore, - IUserClaimStore, - IUserPasswordStore, - IUserSecurityStampStore, - IUserEmailStore, - IUserLockoutStore, - IUserPhoneNumberStore + public class InMemoryUserStore : + IUserStore, + IUserLoginStore, + IUserRoleStore, + IUserClaimStore, + IUserPasswordStore, + IUserSecurityStampStore, + IUserEmailStore, + IUserLockoutStore, + IUserPhoneNumberStore + where TUser : InMemoryUser { - private readonly Dictionary _logins = - new Dictionary(new LoginComparer()); + private readonly Dictionary _logins = + new Dictionary(new LoginComparer()); - private readonly Dictionary _users = new Dictionary(); + private readonly Dictionary _users = new Dictionary(); - public IQueryable Users + public IQueryable Users { get { return _users.Values.AsQueryable(); } } - public Task> GetClaims(InMemoryUser user) + public Task> GetClaims(TUser user) { return Task.FromResult(user.Claims); } - public Task AddClaim(InMemoryUser user, Claim claim) + public Task AddClaim(TUser user, Claim claim) { user.Claims.Add(claim); return Task.FromResult(0); } - public Task RemoveClaim(InMemoryUser user, Claim claim) + public Task RemoveClaim(TUser user, Claim claim) { user.Claims.Remove(claim); return Task.FromResult(0); } - public Task AddLogin(InMemoryUser user, UserLoginInfo login) + public Task AddLogin(TUser user, UserLoginInfo login) { user.Logins.Add(login); _logins[login] = user; return Task.FromResult(0); } - public Task RemoveLogin(InMemoryUser user, UserLoginInfo login) + public Task RemoveLogin(TUser user, UserLoginInfo login) { var logs = - user.Logins.Where(l => l.ProviderKey == login.ProviderKey && l.LoginProvider == login.LoginProvider); + user.Logins.Where(l => l.ProviderKey == login.ProviderKey && l.LoginProvider == login.LoginProvider) + .ToList(); foreach (var l in logs) { user.Logins.Remove(l); @@ -67,100 +69,100 @@ namespace Microsoft.AspNet.Identity.InMemory return Task.FromResult(0); } - public Task> GetLogins(InMemoryUser user) + public Task> GetLogins(TUser user) { return Task.FromResult(user.Logins); } - public Task Find(UserLoginInfo login) + public Task Find(UserLoginInfo login) { if (_logins.ContainsKey(login)) { return Task.FromResult(_logins[login]); } - return Task.FromResult(null); + return Task.FromResult(null); } - public Task SetPasswordHash(InMemoryUser user, string passwordHash) + public Task SetPasswordHash(TUser user, string passwordHash) { user.PasswordHash = passwordHash; return Task.FromResult(0); } - public Task GetPasswordHash(InMemoryUser user) + public Task GetPasswordHash(TUser user) { return Task.FromResult(user.PasswordHash); } - public Task HasPassword(InMemoryUser user) + public Task HasPassword(TUser user) { return Task.FromResult(user.PasswordHash != null); } - public Task AddToRole(InMemoryUser user, string role) + public Task AddToRole(TUser user, string role) { user.Roles.Add(role); return Task.FromResult(0); } - public Task RemoveFromRole(InMemoryUser user, string role) + public Task RemoveFromRole(TUser user, string role) { user.Roles.Remove(role); return Task.FromResult(0); } - public Task> GetRoles(InMemoryUser user) + public Task> GetRoles(TUser user) { return Task.FromResult(user.Roles); } - public Task IsInRole(InMemoryUser user, string role) + public Task IsInRole(TUser user, string role) { return Task.FromResult(user.Roles.Contains(role)); } - public Task SetSecurityStamp(InMemoryUser user, string stamp) + public Task SetSecurityStamp(TUser user, string stamp) { user.SecurityStamp = stamp; return Task.FromResult(0); } - public Task GetSecurityStamp(InMemoryUser user) + public Task GetSecurityStamp(TUser user) { return Task.FromResult(user.SecurityStamp); } - public Task Create(InMemoryUser user) + public Task Create(TUser user) { _users[user.Id] = user; return Task.FromResult(0); } - public Task Update(InMemoryUser user) + public Task Update(TUser user) { _users[user.Id] = user; return Task.FromResult(0); } - public Task FindById(string userId) + public Task FindById(string userId) { if (_users.ContainsKey(userId)) { return Task.FromResult(_users[userId]); } - return Task.FromResult(null); + return Task.FromResult(null); } public void Dispose() { } - public Task FindByName(string userName) + public Task FindByName(string userName) { return Task.FromResult(Users.FirstOrDefault(u => String.Equals(u.UserName, userName, StringComparison.OrdinalIgnoreCase))); } - public Task Delete(InMemoryUser user) + public Task Delete(TUser user) { if (user == null || !_users.ContainsKey(user.Id)) { @@ -170,89 +172,89 @@ namespace Microsoft.AspNet.Identity.InMemory return Task.FromResult(0); } - public Task SetEmail(InMemoryUser user, string email) + public Task SetEmail(TUser user, string email) { user.Email = email; return Task.FromResult(0); } - public Task GetEmail(InMemoryUser user) + public Task GetEmail(TUser user) { return Task.FromResult(user.Email); } - public Task GetEmailConfirmed(InMemoryUser user) + public Task GetEmailConfirmed(TUser user) { return Task.FromResult(user.EmailConfirmed); } - public Task SetEmailConfirmed(InMemoryUser user, bool confirmed) + public Task SetEmailConfirmed(TUser user, bool confirmed) { user.EmailConfirmed = confirmed; return Task.FromResult(0); } - public Task FindByEmail(string email) + public Task FindByEmail(string email) { return Task.FromResult(Users.FirstOrDefault(u => String.Equals(u.Email, email, StringComparison.OrdinalIgnoreCase))); } - public Task GetLockoutEndDate(InMemoryUser user) + public Task GetLockoutEndDate(TUser user) { return Task.FromResult(user.LockoutEnd); } - public Task SetLockoutEndDate(InMemoryUser user, DateTimeOffset lockoutEnd) + public Task SetLockoutEndDate(TUser user, DateTimeOffset lockoutEnd) { user.LockoutEnd = lockoutEnd; return Task.FromResult(0); } - public Task IncrementAccessFailedCount(InMemoryUser user) + public Task IncrementAccessFailedCount(TUser user) { user.AccessFailedCount++; return Task.FromResult(user.AccessFailedCount); } - public Task ResetAccessFailedCount(InMemoryUser user) + public Task ResetAccessFailedCount(TUser user) { user.AccessFailedCount = 0; return Task.FromResult(0); } - public Task GetAccessFailedCount(InMemoryUser user) + public Task GetAccessFailedCount(TUser user) { return Task.FromResult(user.AccessFailedCount); } - public Task GetLockoutEnabled(InMemoryUser user) + public Task GetLockoutEnabled(TUser user) { return Task.FromResult(user.LockoutEnabled); } - public Task SetLockoutEnabled(InMemoryUser user, bool enabled) + public Task SetLockoutEnabled(TUser user, bool enabled) { user.LockoutEnabled = enabled; return Task.FromResult(0); } - public Task SetPhoneNumber(InMemoryUser user, string phoneNumber) + public Task SetPhoneNumber(TUser user, string phoneNumber) { user.PhoneNumber = phoneNumber; return Task.FromResult(0); } - public Task GetPhoneNumber(InMemoryUser user) + public Task GetPhoneNumber(TUser user) { return Task.FromResult(user.PhoneNumber); } - public Task GetPhoneNumberConfirmed(InMemoryUser user) + public Task GetPhoneNumberConfirmed(TUser user) { return Task.FromResult(user.PhoneNumberConfirmed); } - public Task SetPhoneNumberConfirmed(InMemoryUser user, bool confirmed) + public Task SetPhoneNumberConfirmed(TUser user, bool confirmed) { user.PhoneNumberConfirmed = confirmed; return Task.FromResult(0); diff --git a/src/Microsoft.AspNet.Identity/IIdentityValidator.cs b/src/Microsoft.AspNet.Identity/IIdentityValidator.cs deleted file mode 100644 index a0ddf1e319..0000000000 --- a/src/Microsoft.AspNet.Identity/IIdentityValidator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Identity -{ - /// - /// Used to validate an item - /// - /// - public interface IIdentityValidator - { - /// - /// Validate the item - /// - /// - /// - Task Validate(T item); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IPasswordValidator.cs b/src/Microsoft.AspNet.Identity/IPasswordValidator.cs new file mode 100644 index 0000000000..4cf7720165 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IPasswordValidator.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Used to validate passwords + /// + public interface IPasswordValidator + { + /// + /// Validate the item + /// + /// + Task Validate(string password); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IRoleValidator.cs b/src/Microsoft.AspNet.Identity/IRoleValidator.cs new file mode 100644 index 0000000000..400182e995 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IRoleValidator.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Used to validate a role + /// + /// + public interface IRoleValidator + { + /// + /// Validate the user + /// + /// + /// + Task Validate(T role); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserValidator.cs b/src/Microsoft.AspNet.Identity/IUserValidator.cs new file mode 100644 index 0000000000..0480f91b1a --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserValidator.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Used to validate a user + /// + /// + public interface IUserValidator + { + /// + /// Validate the user + /// + /// + /// + Task Validate(T user); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/PasswordValidator.cs b/src/Microsoft.AspNet.Identity/PasswordValidator.cs new file mode 100644 index 0000000000..3615808c64 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/PasswordValidator.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Used to validate some basic password policy like length and number of non alphanumerics + /// + public class PasswordValidator : IPasswordValidator + { + /// + /// Minimum required length + /// + public int RequiredLength { get; set; } + + /// + /// Require a non letter or digit character + /// + public bool RequireNonLetterOrDigit { get; set; } + + /// + /// Require a lower case letter ('a' - 'z') + /// + public bool RequireLowercase { get; set; } + + /// + /// Require an upper case letter ('A' - 'Z') + /// + public bool RequireUppercase { get; set; } + + /// + /// Require a digit ('0' - '9') + /// + public bool RequireDigit { get; set; } + + /// + /// Ensures that the string is of the required length and meets the configured requirements + /// + /// + /// + public virtual Task Validate(string item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + var errors = new List(); + if (string.IsNullOrWhiteSpace(item) || item.Length < RequiredLength) + { + errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.PasswordTooShort, RequiredLength)); + } + if (RequireNonLetterOrDigit && item.All(IsLetterOrDigit)) + { + errors.Add(Resources.PasswordRequireNonLetterOrDigit); + } + if (RequireDigit && item.All(c => !IsDigit(c))) + { + errors.Add(Resources.PasswordRequireDigit); + } + if (RequireLowercase && item.All(c => !IsLower(c))) + { + errors.Add(Resources.PasswordRequireLower); + } + if (RequireUppercase && item.All(c => !IsUpper(c))) + { + errors.Add(Resources.PasswordRequireUpper); + } + if (errors.Count == 0) + { + return Task.FromResult(IdentityResult.Success); + } + return Task.FromResult(IdentityResult.Failed(String.Join(" ", errors))); + } + + /// + /// Returns true if the character is a digit between '0' and '9' + /// + /// + /// + public virtual bool IsDigit(char c) + { + return c >= '0' && c <= '9'; + } + + /// + /// Returns true if the character is between 'a' and 'z' + /// + /// + /// + public virtual bool IsLower(char c) + { + return c >= 'a' && c <= 'z'; + } + + /// + /// Returns true if the character is between 'A' and 'Z' + /// + /// + /// + public virtual bool IsUpper(char c) + { + return c >= 'A' && c <= 'Z'; + } + + /// + /// Returns true if the character is upper, lower, or a digit + /// + /// + /// + public virtual bool IsLetterOrDigit(char c) + { + return IsUpper(c) || IsLower(c) || IsDigit(c); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/RoleManager.cs b/src/Microsoft.AspNet.Identity/RoleManager.cs new file mode 100644 index 0000000000..feea399ca5 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/RoleManager.cs @@ -0,0 +1,213 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Exposes role related api which will automatically save changes to the RoleStore + /// + /// + public class RoleManager : RoleManager where TRole : class, IRole + { + /// + /// Constructor + /// + /// + public RoleManager(IRoleStore store) + : base(store) + { + } + } + + /// + /// Exposes role related api which will automatically save changes to the RoleStore + /// + /// + /// + public class RoleManager : IDisposable + where TRole : class, IRole + where TKey : IEquatable + { + private bool _disposed; + + /// + /// Constructor + /// + /// The IRoleStore is responsible for commiting changes via the UpdateAsync/CreateAsync methods + public RoleManager(IRoleStore store) + { + if (store == null) + { + throw new ArgumentNullException("store"); + } + Store = store; + RoleValidator = new RoleValidator(this); + } + + /// + /// Persistence abstraction that the Manager operates against + /// + protected IRoleStore Store { get; private set; } + + /// + /// Used to validate roles before persisting changes + /// + public IRoleValidator RoleValidator { get; set; } + + /// + /// Returns an IQueryable of roles if the store is an IQueryableRoleStore + /// + public virtual IQueryable Roles + { + get + { + var queryableStore = Store as IQueryableRoleStore; + if (queryableStore == null) + { + throw new NotSupportedException(Resources.StoreNotIQueryableRoleStore); + } + return queryableStore.Roles; + } + } + + /// + /// Dispose this object + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private async Task ValidateRoleInternal(TRole role) + { + return (RoleValidator == null) ? IdentityResult.Success : await RoleValidator.Validate(role).ConfigureAwait(false); + } + + /// + /// Create a role + /// + /// + /// + public virtual async Task Create(TRole role) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException("role"); + } + + var result = await ValidateRoleInternal(role); + if (!result.Succeeded) + { + return result; + } + await Store.Create(role); + return IdentityResult.Success; + } + + /// + /// Update an existing role + /// + /// + /// + public virtual async Task Update(TRole role) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException("role"); + } + + var result = await ValidateRoleInternal(role); + if (!result.Succeeded) + { + return result; + } + await Store.Update(role).ConfigureAwait(false); + return IdentityResult.Success; + } + + /// + /// Delete a role + /// + /// + /// + public virtual async Task Delete(TRole role) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException("role"); + } + + await Store.Delete(role).ConfigureAwait(false); + return IdentityResult.Success; + } + + /// + /// Returns true if the role exists + /// + /// + /// + public virtual async Task RoleExists(string roleName) + { + ThrowIfDisposed(); + if (roleName == null) + { + throw new ArgumentNullException("roleName"); + } + + return await FindByName(roleName).ConfigureAwait(false) != null; + } + + /// + /// Find a role by id + /// + /// + /// + public virtual async Task FindById(TKey roleId) + { + ThrowIfDisposed(); + return await Store.FindById(roleId).ConfigureAwait(false); + } + + /// + /// Find a role by name + /// + /// + /// + public virtual async Task FindByName(string roleName) + { + ThrowIfDisposed(); + if (roleName == null) + { + throw new ArgumentNullException("roleName"); + } + + return await Store.FindByName(roleName).ConfigureAwait(false); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + /// When disposing, actually dipose the store + /// + /// + protected virtual void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + Store.Dispose(); + } + _disposed = true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/RoleValidator.cs b/src/Microsoft.AspNet.Identity/RoleValidator.cs new file mode 100644 index 0000000000..3d3bd3a877 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/RoleValidator.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Validates roles before they are saved + /// + /// + /// + public class RoleValidator : IRoleValidator + where TRole : class, IRole + where TKey : IEquatable + { + /// + /// Constructor + /// + /// + public RoleValidator(RoleManager manager) + { + if (manager == null) + { + throw new ArgumentNullException("manager"); + } + Manager = manager; + } + + private RoleManager Manager { get; set; } + + /// + /// Validates a role before saving + /// + /// + /// + public virtual async Task Validate(TRole item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + var errors = new List(); + await ValidateRoleName(item, errors); + if (errors.Count > 0) + { + return IdentityResult.Failed(errors.ToArray()); + } + return IdentityResult.Success; + } + + private async Task ValidateRoleName(TRole role, List errors) + { + if (string.IsNullOrWhiteSpace(role.Name)) + { + errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.PropertyTooShort, "Name")); + } + else + { + var owner = await Manager.FindByName(role.Name); + if (owner != null && !EqualityComparer.Default.Equals(owner.Id, role.Id)) + { + errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.DuplicateName, role.Name)); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UserManager.cs b/src/Microsoft.AspNet.Identity/UserManager.cs index bbd96f3411..7749affe20 100644 --- a/src/Microsoft.AspNet.Identity/UserManager.cs +++ b/src/Microsoft.AspNet.Identity/UserManager.cs @@ -29,8 +29,6 @@ namespace Microsoft.AspNet.Identity private TimeSpan _defaultLockout = TimeSpan.Zero; private bool _disposed; private IPasswordHasher _passwordHasher; - private IIdentityValidator _passwordValidator; - private IIdentityValidator _userValidator; /// /// Constructor which takes a service provider to find the default interfaces to hook up @@ -45,6 +43,8 @@ namespace Microsoft.AspNet.Identity Store = serviceProvider.GetService>(); ClaimsIdentityFactory = serviceProvider.GetService>(); PasswordHasher = serviceProvider.GetService(); + UserValidator = serviceProvider.GetService>(); + PasswordValidator = serviceProvider.GetService(); // TODO: validator interfaces, and maybe each optional store as well? Email and SMS services? } @@ -59,8 +59,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("store"); } Store = store; - //UserValidator = new UserValidator(this); - //PasswordValidator = new MinimumLengthValidator(6); + UserValidator = new UserValidator(this); //PasswordHasher = new PasswordHasher(); //ClaimsIdentityFactory = new ClaimsIdentityFactory(); } @@ -94,44 +93,12 @@ namespace Microsoft.AspNet.Identity /// /// Used to validate users before persisting changes /// - public IIdentityValidator UserValidator - { - get - { - ThrowIfDisposed(); - return _userValidator; - } - set - { - ThrowIfDisposed(); - if (value == null) - { - throw new ArgumentNullException("value"); - } - _userValidator = value; - } - } + public IUserValidator UserValidator { get; set; } /// /// Used to validate passwords before persisting changes /// - public IIdentityValidator PasswordValidator - { - get - { - ThrowIfDisposed(); - return _passwordValidator; - } - set - { - ThrowIfDisposed(); - if (value == null) - { - throw new ArgumentNullException("value"); - } - _passwordValidator = value; - } - } + public IPasswordValidator PasswordValidator { get; set; } /// /// Used to create claims identities from users @@ -359,6 +326,10 @@ namespace Microsoft.AspNet.Identity return ClaimsIdentityFactory.Create(this, user, authenticationType); } + private async Task ValidateUserInternal(TUser user) { + return (UserValidator == null) ? IdentityResult.Success : await UserValidator.Validate(user).ConfigureAwait(false); + } + /// /// Create a user with no password /// @@ -368,7 +339,7 @@ namespace Microsoft.AspNet.Identity { ThrowIfDisposed(); await UpdateSecurityStampInternal(user).ConfigureAwait(false); - var result = await UserValidator.Validate(user).ConfigureAwait(false); + var result = await ValidateUserInternal(user); if (!result.Succeeded) { return result; @@ -393,8 +364,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - - var result = await UserValidator.Validate(user).ConfigureAwait(false); + var result = await ValidateUserInternal(user); if (!result.Succeeded) { return result; @@ -612,10 +582,13 @@ namespace Microsoft.AspNet.Identity internal async Task UpdatePasswordInternal(IUserPasswordStore passwordStore, TUser user, string newPassword) { - var result = await PasswordValidator.Validate(newPassword).ConfigureAwait(false); - if (!result.Succeeded) + if (PasswordValidator != null) { - return result; + var result = await PasswordValidator.Validate(newPassword).ConfigureAwait(false); + if (!result.Succeeded) + { + return result; + } } await passwordStore.SetPasswordHash(user, PasswordHasher.HashPassword(newPassword)).ConfigureAwait(false); diff --git a/src/Microsoft.AspNet.Identity/UserValidator.cs b/src/Microsoft.AspNet.Identity/UserValidator.cs new file mode 100644 index 0000000000..bd96c8c25f --- /dev/null +++ b/src/Microsoft.AspNet.Identity/UserValidator.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Mail; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Validates users before they are saved + /// + /// + /// + public class UserValidator : IUserValidator + where TUser : class, IUser + where TKey : IEquatable + { + /// + /// Constructor + /// + /// + public UserValidator(UserManager manager) + { + if (manager == null) + { + throw new ArgumentNullException("manager"); + } + AllowOnlyAlphanumericUserNames = true; + Manager = manager; + } + + /// + /// Only allow [A-Za-z0-9@_] in UserNames + /// + public bool AllowOnlyAlphanumericUserNames { get; set; } + + /// + /// If set, enforces that emails are non empty, valid, and unique + /// + public bool RequireUniqueEmail { get; set; } + + private UserManager Manager { get; set; } + + /// + /// Validates a user before saving + /// + /// + /// + public virtual async Task Validate(TUser item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + var errors = new List(); + await ValidateUserName(item, errors); + if (RequireUniqueEmail) + { + await ValidateEmail(item, errors); + } + if (errors.Count > 0) + { + return IdentityResult.Failed(errors.ToArray()); + } + return IdentityResult.Success; + } + + private async Task ValidateUserName(TUser user, List errors) + { + if (string.IsNullOrWhiteSpace(user.UserName)) + { + errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.PropertyTooShort, "Name")); + } + else if (AllowOnlyAlphanumericUserNames && !Regex.IsMatch(user.UserName, @"^[A-Za-z0-9@_\.]+$")) + { + // If any characters are not letters or digits, its an illegal user name + errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.InvalidUserName, user.UserName)); + } + else + { + var owner = await Manager.FindByName(user.UserName); + if (owner != null && !EqualityComparer.Default.Equals(owner.Id, user.Id)) + { + errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.DuplicateName, user.UserName)); + } + } + } + + // make sure email is not empty, valid, and unique + private async Task ValidateEmail(TUser user, List errors) + { + var email = await Manager.GetEmailStore().GetEmail(user).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(email)) + { + errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.PropertyTooShort, "Email")); + return; + } + try + { + var m = new MailAddress(email); + } + catch (FormatException) + { + errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.InvalidEmail, email)); + return; + } + var owner = await Manager.FindByEmail(email); + if (owner != null && !EqualityComparer.Default.Equals(owner.Id, user.Id)) + { + errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.DuplicateEmail, email)); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStoreTest.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStoreTest.cs new file mode 100644 index 0000000000..1e889bbc76 --- /dev/null +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStoreTest.cs @@ -0,0 +1,691 @@ +using System; +using System.Linq; +#if NET45 +using System.Security.Claims; +#else +using System.Security.ClaimsK; +#endif +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNet.Identity.InMemory.Test +{ + public class InMemoryStoreTest + { + [Fact] + public async Task DeleteUserTest() + { + var manager = CreateManager(); + var user = new InMemoryUser("Delete"); + UnitTestHelper.IsSuccess(await manager.Create(user)); + UnitTestHelper.IsSuccess(await manager.Delete(user)); + Assert.Null(await manager.FindById(user.Id)); + } + + [Fact] + public async Task CreateUserNoPasswordTest() + { + var manager = CreateManager(); + UnitTestHelper.IsSuccess(await manager.Create(new InMemoryUser("CreateUserTest"))); + var user = await manager.FindByName("CreateUserTest"); + Assert.NotNull(user); + Assert.Null(user.PasswordHash); + var logins = await manager.GetLogins(user.Id); + Assert.NotNull(logins); + Assert.Equal(0, logins.Count()); + } + + [Fact] + public async Task CreateUserAddLoginTest() + { + var manager = CreateManager(); + const string userName = "CreateExternalUserTest"; + const string provider = "ZzAuth"; + const string providerKey = "HaoKey"; + UnitTestHelper.IsSuccess(await manager.Create(new InMemoryUser(userName))); + var user = await manager.FindByName(userName); + var login = new UserLoginInfo(provider, providerKey); + UnitTestHelper.IsSuccess(await manager.AddLogin(user.Id, login)); + var logins = await manager.GetLogins(user.Id); + Assert.NotNull(logins); + Assert.Equal(1, logins.Count()); + Assert.Equal(provider, logins.First().LoginProvider); + Assert.Equal(providerKey, logins.First().ProviderKey); + } + + //[Fact] + //public async Task CreateUserLoginAndAddPasswordTest() + //{ + // var manager = CreateManager(); + // var login = new UserLoginInfo("Provider", "key"); + // var user = new InMemoryUser("CreateUserLoginAddPasswordTest"); + // UnitTestHelper.IsSuccess(await manager.Create(user)); + // UnitTestHelper.IsSuccess(await manager.AddLogin(user.Id, login)); + // UnitTestHelper.IsSuccess(await manager.AddPassword(user.Id, "password")); + // var logins = await manager.GetLogins(user.Id); + // Assert.NotNull(logins); + // Assert.Equal(1, logins.Count()); + // Assert.Equal(user, await manager.Find(login)); + // Assert.Equal(user, await manager.Find(user.UserName, "password")); + //} + + [Fact] + public async Task CreateUserAddRemoveLoginTest() + { + var manager = CreateManager(); + var user = new InMemoryUser("CreateUserAddRemoveLoginTest"); + var login = new UserLoginInfo("Provider", "key"); + var result = await manager.Create(user); + Assert.NotNull(user); + UnitTestHelper.IsSuccess(result); + UnitTestHelper.IsSuccess(await manager.AddLogin(user.Id, login)); + Assert.Equal(user, await manager.Find(login)); + var logins = await manager.GetLogins(user.Id); + Assert.NotNull(logins); + Assert.Equal(1, logins.Count()); + Assert.Equal(login.LoginProvider, logins.Last().LoginProvider); + Assert.Equal(login.ProviderKey, logins.Last().ProviderKey); + var stamp = user.SecurityStamp; + UnitTestHelper.IsSuccess(await manager.RemoveLogin(user.Id, login)); + Assert.Null(await manager.Find(login)); + logins = await manager.GetLogins(user.Id); + Assert.NotNull(logins); + Assert.Equal(0, logins.Count()); + Assert.NotEqual(stamp, user.SecurityStamp); + } + + //[Fact] + //public async Task RemovePasswordTest() + //{ + // var manager = CreateManager(); + // var user = new InMemoryUser("RemovePasswordTest"); + // const string password = "password"; + // UnitTestHelper.IsSuccess(await manager.Create(user, password)); + // var stamp = user.SecurityStamp; + // UnitTestHelper.IsSuccess(await manager.RemovePassword(user.Id)); + // var u = await manager.FindByName(user.UserName); + // Assert.NotNull(u); + // Assert.Null(u.PasswordHash); + // Assert.NotEqual(stamp, user.SecurityStamp); + //} + + //[Fact] + //public async Task ChangePasswordTest() + //{ + // var manager = CreateManager(); + // var user = new InMemoryUser("ChangePasswordTest"); + // const string password = "password"; + // const string newPassword = "newpassword"; + // UnitTestHelper.IsSuccess(await manager.Create(user, password)); + // var stamp = user.SecurityStamp; + // Assert.NotNull(stamp); + // UnitTestHelper.IsSuccess(await manager.ChangePassword(user.Id, password, newPassword)); + // Assert.Null(await manager.Find(user.UserName, password)); + // Assert.Equal(user, await manager.Find(user.UserName, newPassword)); + // Assert.NotEqual(stamp, user.SecurityStamp); + //} + + [Fact] + public async Task AddRemoveUserClaimTest() + { + var manager = CreateManager(); + var user = new InMemoryUser("ClaimsAddRemove"); + UnitTestHelper.IsSuccess(await manager.Create(user)); + Claim[] claims = { new Claim("c", "v"), new Claim("c2", "v2"), new Claim("c2", "v3") }; + foreach (Claim c in claims) + { + UnitTestHelper.IsSuccess(await manager.AddClaim(user.Id, c)); + } + var userClaims = await manager.GetClaims(user.Id); + Assert.Equal(3, userClaims.Count); + UnitTestHelper.IsSuccess(await manager.RemoveClaim(user.Id, claims[0])); + userClaims = await manager.GetClaims(user.Id); + Assert.Equal(2, userClaims.Count); + UnitTestHelper.IsSuccess(await manager.RemoveClaim(user.Id, claims[1])); + userClaims = await manager.GetClaims(user.Id); + Assert.Equal(1, userClaims.Count); + UnitTestHelper.IsSuccess(await manager.RemoveClaim(user.Id, claims[2])); + userClaims = await manager.GetClaims(user.Id); + Assert.Equal(0, userClaims.Count); + } + + //[Fact] + //public async Task ChangePasswordFallsIfPasswordTooShortTest() + //{ + // var manager = CreateManager(); + // var user = new InMemoryUser("user"); + // const string password = "password"; + // UnitTestHelper.IsSuccess(await manager.Create(user, password)); + // var result = await manager.ChangePassword(user.Id, password, "n"); + // UnitTestHelper.IsFailure(result, "Passwords must be at least 6 characters."); + //} + + //[Fact] + //public async Task ChangePasswordFallsIfPasswordWrongTest() + //{ + // var manager = CreateManager(); + // var user = new InMemoryUser("user"); + // UnitTestHelper.IsSuccess(await manager.Create(user, "password")); + // var result = await manager.ChangePassword(user.Id, "bogus", "newpassword"); + // UnitTestHelper.IsFailure(result, "Incorrect password."); + //} + + [Fact] + public async Task AddDupeUserFailsTest() + { + var manager = CreateManager(); + var user = new InMemoryUser("dupe"); + var user2 = new InMemoryUser("dupe"); + UnitTestHelper.IsSuccess(await manager.Create(user)); + UnitTestHelper.IsFailure(await manager.Create(user2), "Name dupe is already taken."); + } + + [Fact] + public async Task UpdateSecurityStampTest() + { + var manager = CreateManager(); + var user = new InMemoryUser("stampMe"); + Assert.Null(user.SecurityStamp); + UnitTestHelper.IsSuccess(await manager.Create(user)); + var stamp = user.SecurityStamp; + Assert.NotNull(stamp); + UnitTestHelper.IsSuccess(await manager.UpdateSecurityStamp(user.Id)); + Assert.NotEqual(stamp, user.SecurityStamp); + } + + //[Fact] + //public async Task AddDupeLoginFailsTest() + //{ + // var manager = CreateManager(); + // var user = new InMemoryUser("DupeLogin"); + // var login = new UserLoginInfo("provder", "key"); + // UnitTestHelper.IsSuccess(await manager.Create(user)); + // UnitTestHelper.IsSuccess(await manager.AddLogin(user.Id, login)); + // var result = await manager.AddLogin(user.Id, login); + // UnitTestHelper.IsFailure(result, "A user with that external login already exists."); + //} + + // Lockout tests + + [Fact] + public async Task SingleFailureLockout() + { + var mgr = CreateManager(); + mgr.DefaultAccountLockoutTimeSpan = TimeSpan.FromHours(1); + mgr.UserLockoutEnabledByDefault = true; + var user = new InMemoryUser("fastLockout"); + UnitTestHelper.IsSuccess(await mgr.Create(user)); + Assert.True(await mgr.GetLockoutEnabled(user.Id)); + Assert.True(user.LockoutEnabled); + Assert.False(await mgr.IsLockedOut(user.Id)); + UnitTestHelper.IsSuccess(await mgr.AccessFailed(user.Id)); + Assert.True(await mgr.IsLockedOut(user.Id)); + Assert.True(await mgr.GetLockoutEndDate(user.Id) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(0, await mgr.GetAccessFailedCount(user.Id)); + } + + [Fact] + public async Task TwoFailureLockout() + { + var mgr = CreateManager(); + mgr.DefaultAccountLockoutTimeSpan = TimeSpan.FromHours(1); + mgr.UserLockoutEnabledByDefault = true; + mgr.MaxFailedAccessAttemptsBeforeLockout = 2; + var user = new InMemoryUser("twoFailureLockout"); + UnitTestHelper.IsSuccess(await mgr.Create(user)); + Assert.True(await mgr.GetLockoutEnabled(user.Id)); + Assert.True(user.LockoutEnabled); + Assert.False(await mgr.IsLockedOut(user.Id)); + UnitTestHelper.IsSuccess(await mgr.AccessFailed(user.Id)); + Assert.False(await mgr.IsLockedOut(user.Id)); + Assert.False(await mgr.GetLockoutEndDate(user.Id) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(1, await mgr.GetAccessFailedCount(user.Id)); + UnitTestHelper.IsSuccess(await mgr.AccessFailed(user.Id)); + Assert.True(await mgr.IsLockedOut(user.Id)); + Assert.True(await mgr.GetLockoutEndDate(user.Id) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(0, await mgr.GetAccessFailedCount(user.Id)); + } + + [Fact] + public async Task ResetLockoutTest() + { + var mgr = CreateManager(); + mgr.DefaultAccountLockoutTimeSpan = TimeSpan.FromHours(1); + mgr.UserLockoutEnabledByDefault = true; + mgr.MaxFailedAccessAttemptsBeforeLockout = 2; + var user = new InMemoryUser("resetLockout"); + UnitTestHelper.IsSuccess(await mgr.Create(user)); + Assert.True(await mgr.GetLockoutEnabled(user.Id)); + Assert.True(user.LockoutEnabled); + Assert.False(await mgr.IsLockedOut(user.Id)); + UnitTestHelper.IsSuccess(await mgr.AccessFailed(user.Id)); + Assert.False(await mgr.IsLockedOut(user.Id)); + Assert.False(await mgr.GetLockoutEndDate(user.Id) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(1, await mgr.GetAccessFailedCount(user.Id)); + UnitTestHelper.IsSuccess(await mgr.ResetAccessFailedCount(user.Id)); + Assert.Equal(0, await mgr.GetAccessFailedCount(user.Id)); + Assert.False(await mgr.IsLockedOut(user.Id)); + Assert.False(await mgr.GetLockoutEndDate(user.Id) > DateTimeOffset.UtcNow.AddMinutes(55)); + UnitTestHelper.IsSuccess(await mgr.AccessFailed(user.Id)); + Assert.False(await mgr.IsLockedOut(user.Id)); + Assert.False(await mgr.GetLockoutEndDate(user.Id) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(1, await mgr.GetAccessFailedCount(user.Id)); + } + + [Fact] + public async Task EnableLockoutManually() + { + var mgr = CreateManager(); + mgr.DefaultAccountLockoutTimeSpan = TimeSpan.FromHours(1); + mgr.MaxFailedAccessAttemptsBeforeLockout = 2; + var user = new InMemoryUser("manualLockout"); + UnitTestHelper.IsSuccess(await mgr.Create(user)); + Assert.False(await mgr.GetLockoutEnabled(user.Id)); + Assert.False(user.LockoutEnabled); + UnitTestHelper.IsSuccess(await mgr.SetLockoutEnabled(user.Id, true)); + Assert.True(await mgr.GetLockoutEnabled(user.Id)); + Assert.True(user.LockoutEnabled); + Assert.False(await mgr.IsLockedOut(user.Id)); + UnitTestHelper.IsSuccess(await mgr.AccessFailed(user.Id)); + Assert.False(await mgr.IsLockedOut(user.Id)); + Assert.False(await mgr.GetLockoutEndDate(user.Id) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(1, await mgr.GetAccessFailedCount(user.Id)); + UnitTestHelper.IsSuccess(await mgr.AccessFailed(user.Id)); + Assert.True(await mgr.IsLockedOut(user.Id)); + Assert.True(await mgr.GetLockoutEndDate(user.Id) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(0, await mgr.GetAccessFailedCount(user.Id)); + } + + [Fact] + public async Task UserNotLockedOutWithNullDateTimeAndIsSetToNullDate() + { + var mgr = CreateManager(); + mgr.UserLockoutEnabledByDefault = true; + var user = new InMemoryUser("LockoutTest"); + UnitTestHelper.IsSuccess(await mgr.Create(user)); + Assert.True(await mgr.GetLockoutEnabled(user.Id)); + Assert.True(user.LockoutEnabled); + UnitTestHelper.IsSuccess(await mgr.SetLockoutEndDate(user.Id, new DateTimeOffset())); + Assert.False(await mgr.IsLockedOut(user.Id)); + Assert.Equal(new DateTimeOffset(), await mgr.GetLockoutEndDate(user.Id)); + Assert.Equal(new DateTimeOffset(), user.LockoutEnd); + } + + [Fact] + public async Task LockoutFailsIfNotEnabled() + { + var mgr = CreateManager(); + var user = new InMemoryUser("LockoutNotEnabledTest"); + UnitTestHelper.IsSuccess(await mgr.Create(user)); + Assert.False(await mgr.GetLockoutEnabled(user.Id)); + Assert.False(user.LockoutEnabled); + UnitTestHelper.IsFailure(await mgr.SetLockoutEndDate(user.Id, new DateTimeOffset()), "Lockout is not enabled for this user."); + Assert.False(await mgr.IsLockedOut(user.Id)); + } + + [Fact] + public async Task LockoutEndToUtcNowMinus1SecInUserShouldNotBeLockedOut() + { + var mgr = CreateManager(); + mgr.UserLockoutEnabledByDefault = true; + var user = new InMemoryUser("LockoutUtcNowTest") { LockoutEnd = DateTimeOffset.UtcNow.AddSeconds(-1) }; + UnitTestHelper.IsSuccess(await mgr.Create(user)); + Assert.True(await mgr.GetLockoutEnabled(user.Id)); + Assert.True(user.LockoutEnabled); + Assert.False(await mgr.IsLockedOut(user.Id)); + } + + [Fact] + public async Task LockoutEndToUtcNowSubOneSecondWithManagerShouldNotBeLockedOut() + { + var mgr = CreateManager(); + mgr.UserLockoutEnabledByDefault = true; + var user = new InMemoryUser("LockoutUtcNowTest"); + UnitTestHelper.IsSuccess(await mgr.Create(user)); + Assert.True(await mgr.GetLockoutEnabled(user.Id)); + Assert.True(user.LockoutEnabled); + UnitTestHelper.IsSuccess(await mgr.SetLockoutEndDate(user.Id, DateTimeOffset.UtcNow.AddSeconds(-1))); + Assert.False(await mgr.IsLockedOut(user.Id)); + } + + [Fact] + public async Task LockoutEndToUtcNowPlus5ShouldBeLockedOut() + { + var mgr = CreateManager(); + mgr.UserLockoutEnabledByDefault = true; + var user = new InMemoryUser("LockoutUtcNowTest") { LockoutEnd = DateTimeOffset.UtcNow.AddMinutes(5) }; + UnitTestHelper.IsSuccess(await mgr.Create(user)); + Assert.True(await mgr.GetLockoutEnabled(user.Id)); + Assert.True(user.LockoutEnabled); + Assert.True(await mgr.IsLockedOut(user.Id)); + } + + [Fact] + public async Task UserLockedOutWithDateTimeLocalKindNowPlus30() + { + var mgr = CreateManager(); + mgr.UserLockoutEnabledByDefault = true; + var user = new InMemoryUser("LockoutTest"); + UnitTestHelper.IsSuccess(await mgr.Create(user)); + Assert.True(await mgr.GetLockoutEnabled(user.Id)); + Assert.True(user.LockoutEnabled); + var lockoutEnd = new DateTimeOffset(DateTime.Now.AddMinutes(30).ToLocalTime()); + UnitTestHelper.IsSuccess(await mgr.SetLockoutEndDate(user.Id, lockoutEnd)); + Assert.True(await mgr.IsLockedOut(user.Id)); + var end = await mgr.GetLockoutEndDate(user.Id); + Assert.Equal(lockoutEnd, end); + } + + // Role Tests + [Fact] + public async Task CreateRoleTest() + { + var manager = CreateRoleManager(); + var role = new InMemoryRole("create"); + Assert.False(await manager.RoleExists(role.Name)); + UnitTestHelper.IsSuccess(await manager.Create(role)); + Assert.True(await manager.RoleExists(role.Name)); + } + + //[Fact] + //public async Task BadValidatorBlocksCreateTest() + //{ + // var manager = CreateRoleManager(); + // manager.RoleValidator = new AlwaysBadValidator(); + // UnitTestHelper.IsFailure(await manager.Create(new InMemoryRole("blocked")), + // AlwaysBadValidator.ErrorMessage); + //} + + //[Fact] + //public async Task BadValidatorBlocksAllUpdatesTest() + //{ + // var manager = CreateRoleManager(); + // var role = new InMemoryRole("poorguy"); + // UnitTestHelper.IsSuccess(await manager.Create(role)); + // var error = AlwaysBadValidator.ErrorMessage; + // manager.RoleValidator = new AlwaysBadValidator(); + // UnitTestHelper.IsFailure(await manager.Update(role), error); + //} + + [Fact] + public async Task DeleteRoleTest() + { + var manager = CreateRoleManager(); + var role = new InMemoryRole("delete"); + Assert.False(await manager.RoleExists(role.Name)); + UnitTestHelper.IsSuccess(await manager.Create(role)); + UnitTestHelper.IsSuccess(await manager.Delete(role)); + Assert.False(await manager.RoleExists(role.Name)); + } + + [Fact] + public async Task RoleFindByIdTest() + { + var manager = CreateRoleManager(); + var role = new InMemoryRole("FindById"); + Assert.Null(await manager.FindById(role.Id)); + UnitTestHelper.IsSuccess(await manager.Create(role)); + Assert.Equal(role, await manager.FindById(role.Id)); + } + + [Fact] + public async Task RoleFindByNameTest() + { + var manager = CreateRoleManager(); + var role = new InMemoryRole("FindByName"); + Assert.Null(await manager.FindByName(role.Name)); + Assert.False(await manager.RoleExists(role.Name)); + UnitTestHelper.IsSuccess(await manager.Create(role)); + Assert.Equal(role, await manager.FindByName(role.Name)); + } + + [Fact] + public async Task UpdateRoleNameTest() + { + var manager = CreateRoleManager(); + var role = new InMemoryRole("update"); + Assert.False(await manager.RoleExists(role.Name)); + UnitTestHelper.IsSuccess(await manager.Create(role)); + Assert.True(await manager.RoleExists(role.Name)); + role.Name = "Changed"; + UnitTestHelper.IsSuccess(await manager.Update(role)); + Assert.False(await manager.RoleExists("update")); + Assert.Equal(role, await manager.FindByName(role.Name)); + } + + [Fact] + public async Task QuerableRolesTest() + { + var manager = CreateRoleManager(); + InMemoryRole[] roles = + { + new InMemoryRole("r1"), new InMemoryRole("r2"), new InMemoryRole("r3"), + new InMemoryRole("r4") + }; + foreach (var r in roles) + { + UnitTestHelper.IsSuccess(await manager.Create(r)); + } + Assert.Equal(roles.Length, manager.Roles.Count()); + var r1 = manager.Roles.FirstOrDefault(r => r.Name == "r1"); + Assert.Equal(roles[0], r1); + } + + //[Fact] + //public async Task DeleteRoleNonEmptySucceedsTest() + //{ + // // Need fail if not empty? + // var userMgr = CreateManager(); + // var roleMgr = CreateRoleManager(); + // var role = new InMemoryRole("deleteNonEmpty"); + // Assert.False(await roleMgr.RoleExists(role.Name)); + // UnitTestHelper.IsSuccess(await roleMgr.Create(role)); + // var user = new InMemoryUser("t"); + // UnitTestHelper.IsSuccess(await userMgr.Create(user)); + // UnitTestHelper.IsSuccess(await userMgr.AddToRole(user.Id, role.Name)); + // UnitTestHelper.IsSuccess(await roleMgr.Delete(role)); + // Assert.Null(await roleMgr.FindByName(role.Name)); + // Assert.False(await roleMgr.RoleExists(role.Name)); + // // REVIEW: We should throw if deleteing a non empty role? + // var roles = await userMgr.GetRoles(user.Id); + + // // In memory this doesn't work since there's no concept of cascading deletes + // //Assert.Equal(0, roles.Count()); + //} + + ////[Fact] + ////public async Task DeleteUserRemovesFromRoleTest() + ////{ + //// // Need fail if not empty? + //// var userMgr = CreateManager(); + //// var roleMgr = CreateRoleManager(); + //// var role = new InMemoryRole("deleteNonEmpty"); + //// Assert.False(await roleMgr.RoleExists(role.Name)); + //// UnitTestHelper.IsSuccess(await roleMgr.Create(role)); + //// var user = new InMemoryUser("t"); + //// UnitTestHelper.IsSuccess(await userMgr.Create(user)); + //// UnitTestHelper.IsSuccess(await userMgr.AddToRole(user.Id, role.Name)); + //// UnitTestHelper.IsSuccess(await userMgr.Delete(user)); + //// role = roleMgr.FindById(role.Id); + ////} + + [Fact] + public async Task CreateRoleFailsIfExistsTest() + { + var manager = CreateRoleManager(); + var role = new InMemoryRole("dupeRole"); + Assert.False(await manager.RoleExists(role.Name)); + UnitTestHelper.IsSuccess(await manager.Create(role)); + Assert.True(await manager.RoleExists(role.Name)); + var role2 = new InMemoryRole("dupeRole"); + UnitTestHelper.IsFailure(await manager.Create(role2)); + } + + [Fact] + public async Task AddUserToRoleTest() + { + var manager = CreateManager(); + var roleManager = CreateRoleManager(); + var role = new InMemoryRole("addUserTest"); + UnitTestHelper.IsSuccess(await roleManager.Create(role)); + InMemoryUser[] users = + { + new InMemoryUser("1"), new InMemoryUser("2"), new InMemoryUser("3"), + new InMemoryUser("4") + }; + foreach (InMemoryUser u in users) + { + UnitTestHelper.IsSuccess(await manager.Create(u)); + UnitTestHelper.IsSuccess(await manager.AddToRole(u.Id, role.Name)); + Assert.True(await manager.IsInRole(u.Id, role.Name)); + } + } + + [Fact] + public async Task GetRolesForUserTest() + { + var userManager = CreateManager(); + var roleManager = CreateRoleManager(); + InMemoryUser[] users = + { + new InMemoryUser("u1"), new InMemoryUser("u2"), new InMemoryUser("u3"), + new InMemoryUser("u4") + }; + InMemoryRole[] roles = + { + new InMemoryRole("r1"), new InMemoryRole("r2"), new InMemoryRole("r3"), + new InMemoryRole("r4") + }; + foreach (var u in users) + { + UnitTestHelper.IsSuccess(await userManager.Create(u)); + } + foreach (var r in roles) + { + UnitTestHelper.IsSuccess(await roleManager.Create(r)); + foreach (var u in users) + { + UnitTestHelper.IsSuccess(await userManager.AddToRole(u.Id, r.Name)); + Assert.True(await userManager.IsInRole(u.Id, r.Name)); + } + } + + foreach (var u in users) + { + var rs = await userManager.GetRoles(u.Id); + Assert.Equal(roles.Length, rs.Count); + foreach (var r in roles) + { + Assert.True(rs.Any(role => role == r.Name)); + } + } + } + + + [Fact] + public async Task RemoveUserFromRoleWithMultipleRoles() + { + var userManager = CreateManager(); + var roleManager = CreateRoleManager(); + var user = new InMemoryUser("MultiRoleUser"); + UnitTestHelper.IsSuccess(await userManager.Create(user)); + InMemoryRole[] roles = + { + new InMemoryRole("r1"), new InMemoryRole("r2"), new InMemoryRole("r3"), + new InMemoryRole("r4") + }; + foreach (var r in roles) + { + UnitTestHelper.IsSuccess(await roleManager.Create(r)); + UnitTestHelper.IsSuccess(await userManager.AddToRole(user.Id, r.Name)); + Assert.True(await userManager.IsInRole(user.Id, r.Name)); + } + UnitTestHelper.IsSuccess(await userManager.RemoveFromRole(user.Id, roles[2].Name)); + Assert.False(await userManager.IsInRole(user.Id, roles[2].Name)); + } + + [Fact] + public async Task RemoveUserFromRoleTest() + { + var userManager = CreateManager(); + var roleManager = CreateRoleManager(); + InMemoryUser[] users = + { + new InMemoryUser("1"), new InMemoryUser("2"), new InMemoryUser("3"), + new InMemoryUser("4") + }; + foreach (var u in users) + { + UnitTestHelper.IsSuccess(await userManager.Create(u)); + } + var r = new InMemoryRole("r1"); + UnitTestHelper.IsSuccess(await roleManager.Create(r)); + foreach (var u in users) + { + UnitTestHelper.IsSuccess(await userManager.AddToRole(u.Id, r.Name)); + Assert.True(await userManager.IsInRole(u.Id, r.Name)); + } + foreach (var u in users) + { + UnitTestHelper.IsSuccess(await userManager.RemoveFromRole(u.Id, r.Name)); + Assert.False(await userManager.IsInRole(u.Id, r.Name)); + } + } + + [Fact] + public async Task RemoveUserNotInRoleFailsTest() + { + var userMgr = CreateManager(); + var roleMgr = CreateRoleManager(); + var role = new InMemoryRole("addUserDupeTest"); + var user = new InMemoryUser("user1"); + UnitTestHelper.IsSuccess(await userMgr.Create(user)); + UnitTestHelper.IsSuccess(await roleMgr.Create(role)); + var result = await userMgr.RemoveFromRole(user.Id, role.Name); + UnitTestHelper.IsFailure(result, "User is not in role."); + } + + [Fact] + public async Task AddUserToRoleFailsIfAlreadyInRoleTest() + { + var userMgr = CreateManager(); + var roleMgr = CreateRoleManager(); + var role = new InMemoryRole("addUserDupeTest"); + var user = new InMemoryUser("user1"); + UnitTestHelper.IsSuccess(await userMgr.Create(user)); + UnitTestHelper.IsSuccess(await roleMgr.Create(role)); + UnitTestHelper.IsSuccess(await userMgr.AddToRole(user.Id, role.Name)); + Assert.True(await userMgr.IsInRole(user.Id, role.Name)); + UnitTestHelper.IsFailure(await userMgr.AddToRole(user.Id, role.Name), "User already in role."); + } + + [Fact] + public async Task FindRoleByNameWithManagerTest() + { + var roleMgr = CreateRoleManager(); + var role = new InMemoryRole("findRoleByNameTest"); + UnitTestHelper.IsSuccess(await roleMgr.Create(role)); + Assert.Equal(role.Id, (await roleMgr.FindByName(role.Name)).Id); + } + + [Fact] + public async Task FindRoleWithManagerTest() + { + var roleMgr = CreateRoleManager(); + var role = new InMemoryRole("findRoleTest"); + UnitTestHelper.IsSuccess(await roleMgr.Create(role)); + Assert.Equal(role.Name, (await roleMgr.FindById(role.Id)).Name); + } + + + private static UserManager CreateManager() + { + return new UserManager(new InMemoryUserStore()); + } + + private static RoleManager CreateRoleManager() + { + return new RoleManager(new InMemoryRoleStore()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/UnitTestHelper.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/UnitTestHelper.cs new file mode 100644 index 0000000000..a2e1d786d4 --- /dev/null +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/UnitTestHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using Microsoft.AspNet.Identity; +using Xunit; + +namespace Microsoft.AspNet.Identity.InMemory.Test +{ + public static class UnitTestHelper + { + public static void IsSuccess(IdentityResult result) + { + Assert.NotNull(result); + Assert.True(result.Succeeded); + } + + public static void IsFailure(IdentityResult result) + { + Assert.NotNull(result); + Assert.False(result.Succeeded); + } + + public static void IsFailure(IdentityResult result, string error) + { + Assert.NotNull(result); + Assert.False(result.Succeeded); + Assert.Equal(error, result.Errors.First()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/project.json b/test/Microsoft.AspNet.Identity.InMemory.Test/project.json new file mode 100644 index 0000000000..00342e9cc1 --- /dev/null +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/project.json @@ -0,0 +1,15 @@ +{ + "version": "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.Identity" : "0.1-alpha-*", + "Microsoft.AspNet.Identity.InMemory" : "0.1-alpha-*", + "Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*" + }, + "configurations": { + "net45": { + "dependencies": { + "xunit": "1.9.2" + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/project.json b/test/Microsoft.AspNet.Identity.Test/project.json new file mode 100644 index 0000000000..ff81ab618e --- /dev/null +++ b/test/Microsoft.AspNet.Identity.Test/project.json @@ -0,0 +1,14 @@ +{ + "version": "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.Identity" : "0.1-alpha-*", + "Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*" + }, + "configurations": { + "net45": { + "dependencies": { + "xunit": "1.9.2" + } + } + } +} \ No newline at end of file