Let's say I have a class like this:
public class CustomAuthorizeAttribute : AuthorizeAttribute { protected override bool AuthorizeCore(HttpContextBase httpContext) { return string.IsNullOrEmpty(Roles) ? base.AuthorizeCore(httpContext) : IsUserInRole(Roles); } public static bool IsUserInRole(string roles) { return roles.Split(',', ';').Any(x => System.Web.Security.Roles.IsUserInRole(x.Trim())); } }This class is a very simple class that depends on a RoleProvider to check whether current user is in one of demanded roles. The question is what I'm suppose to test this class? Hmmm, the System.Web.Security.Roles class has a Providers collection of RoleProvider but it's readonly as well as the property Provider which should be default provider. It looks like there is only one way to put my mock/fake role provider is placing it into configuration file of the Test project. Currently, I'm interested in using NSubstitue as the mocking framework, but I think the following TestRoleProvider can be easily changed to use other mock library like RhinoMock or Moq. Here is my very simple TestRoleProvider that can be configed in the App.config and later on arrange our "Expectation" on it:
public class TestRoleProvider : RoleProvider { private readonly RoleProvider _roleProvider; public TestRoleProvider(RoleProvider provider) { _roleProvider = provider; } public TestRoleProvider() { _roleProvider = Substitute.For<RoleProvider>(); } public override void AddUsersToRoles(string[] usernames, string[] roleNames) { RoleProvider.AddUsersToRoles(usernames, roleNames); } public override string ApplicationName { get { return RoleProvider.ApplicationName; } set { RoleProvider.ApplicationName = value; } } public RoleProvider RoleProvider { get { return _roleProvider; } } public override void CreateRole(string roleName) { RoleProvider.CreateRole(roleName); } public override bool DeleteRole(string roleName, bool throwOnPopulatedRole) { return RoleProvider.DeleteRole(roleName, throwOnPopulatedRole); } public override string[] FindUsersInRole(string roleName, string usernameToMatch) { return RoleProvider.FindUsersInRole(roleName, usernameToMatch); } public override string[] GetAllRoles() { return RoleProvider.GetAllRoles(); } public override string[] GetRolesForUser(string username) { return RoleProvider.GetRolesForUser(username); } public override string[] GetUsersInRole(string roleName) { return RoleProvider.GetUsersInRole(roleName); } public override bool IsUserInRole(string username, string roleName) { return _roleProvider.IsUserInRole(username, roleName); } public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames) { RoleProvider.RemoveUsersFromRoles(usernames, roleNames); } public override bool RoleExists(string roleName) { return RoleProvider.RoleExists(roleName); } }And here is the configuration in App.config to make the test project use this TestRoleProvider as default RoleProvider:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.web> <roleManager enabled="true" defaultProvider="TestRoleProvider"> <providers> <add name="TestRoleProvider" type="MyCompany.MyProject.Tests.TestRoleProvider, MyCompany.MyProject.Tests"/> <!-- Note: Change the namespace, assembly name to the assembly you put TestRoleProvider. --> </providers> </roleManager> </system.web> </configuration>Now, I will write a simple unit test for method CustomAuthorizeAttribute.IsUserInRole(roles):
[TestMethod] public void Can_Check_User_Roles_Using_Role_Provider() { // Arrange var testRoleProvider = Roles.Provider as TestRoleProvider; if (testRoleProvider == null) { throw new Exception("TestRoleProvider must be configed in app.config as default provider for your unit tests"); } testRoleProvider.RoleProvider.IsUserInRole(Arg.Any<string>(), Arg.Any<string>()).Returns(true); // Action var result = CustomAuthorizeAttribute.UserInRole("Admin, Mod"); // Assert Assert.AreEqual(true, result); }And here the result after running above unit test: Look like something's wrong. I swear I had setup the provider correctly in App.config. The Test ran failed and that means "testRoleProvider" is not null because I did not receive any exception. Maybe there are some secrets inside the implementation of System.Web.Security.Roles. Fine, I will find out what they are. It's time to use the power of Reflector but let's download it first. Hmm, newest version is 7 and it has not been free anymore. Ok, I will use previous free version, let's show some googling skill. Hehe, I found some old web addresses that allow me to download Reflector 5.x. Arggg, It forced me to upgrade to 7.0, I chose not to upgrade and It deleted the reflector executable file. All I need is to see the code inside Roles, so free 14 days trial is enough for me :(. And here is the secret:
public static bool IsUserInRole(string username, string roleName) { bool flag3; if (HostingEnvironment.IsHosted && EtwTrace.IsTraceEnabled(4, 8)) { EtwTrace.Trace(EtwTraceType.ETW_TYPE_ROLE_BEGIN, HttpContext.Current.WorkerRequest); } EnsureEnabled(); bool flag = false; bool flag2 = false; try { SecUtility.CheckParameter(ref roleName, true, true, true, 0, "roleName"); SecUtility.CheckParameter(ref username, true, false, true, 0, "username"); if (username.Length < 1) { return false; } IPrincipal currentUser = GetCurrentUser(); if (((currentUser != null) && (currentUser is RolePrincipal)) && ((((RolePrincipal) currentUser).ProviderName == Provider.Name) && StringUtil.EqualsIgnoreCase(username, currentUser.Identity.Name))) { flag = currentUser.IsInRole(roleName); } else { flag = Provider.IsUserInRole(username, roleName); } flag3 = flag; } finally { if (HostingEnvironment.IsHosted && EtwTrace.IsTraceEnabled(4, 8)) { if (EtwTrace.IsTraceEnabled(5, 8)) { string str = SR.Resources.GetString(flag ? "Etw_Success" : "Etw_Failure", CultureInfo.InstalledUICulture); EtwTrace.Trace(EtwTraceType.ETW_TYPE_ROLE_IS_USER_IN_ROLE, HttpContext.Current.WorkerRequest, flag2 ? "RolePrincipal" : Provider.GetType().FullName, username, roleName, str); } EtwTrace.Trace(EtwTraceType.ETW_TYPE_ROLE_END, HttpContext.Current.WorkerRequest, flag2 ? "RolePrincipal" : Provider.GetType().FullName, username); } } return flag3; }And this is the way it gets current user:
private static IPrincipal GetCurrentUser() { if (HostingEnvironment.IsHosted) { HttpContext current = HttpContext.Current; if (current != null) { return current.User; } } return Thread.CurrentPrincipal; }Okey, i have enough information to let the test use my roleprovider. It's quite complicated to modify HostingEnvironment.IsHosted as well as HttpContext.Current. Indeed, I can create a mock httpContext and set the mock object to HttpContext.Current but I think changing CurrentPrincipal of current Thread is more simple. So, I decided to change my unit test arrangement:
[TestMethod] public void Can_Check_User_Roles_Using_Role_Provider() { // Arrange var identity = Substitute.For<IIdentity>(); identity.IsAuthenticated.Returns(true); identity.Name.Returns("AnyUsername"); var principal = Substitute.For<IPrincipal>(); principal.Identity.Returns(identity); Thread.CurrentPrincipal = principal; var testRoleProvider = Roles.Provider as TestRoleProvider; if (testRoleProvider == null) { throw new Exception("TestRoleProvider must be configed in app.config as default provider for your unit tests"); } testRoleProvider.RoleProvider.IsUserInRole(Arg.Any<string>(), Arg.Any<string>()).Returns(true); // Action var result = CustomAuthorizeAttribute.IsUserInRole("Admin, Mod"); // Assert Assert.AreEqual(true, result); }Hoho, finally It passed. Reflector is so useful, isn't it? I think we can do the same way if we want to test any implementations that need MembershipProvider or ProfileProvider. I would move the Principal Arrangement into a public static method of TestRoleProvider to make it reusable. And because I changed the Thread.CurrentPrincipal during the test, so it could be nice if we make a test initialize and test cleanup to backup/restore current principle.But I let you to do those stuff if you're interested in :D Cheers.
2 comments:
Nice work - I was rather stuck trying to solve just this problem, where I had a custom IPrincipal, IIdentity, MembershipProvider and RoleProvider. Your code has pushed me in the right direction. Thanks!
Thanks! Your article helped me a lot to build up my test environment
Post a Comment