I am quite new to Unit testing and Mock concepts. I am trying to figure out how to write a good test case for the basic out-of-the box user registration code below:
[HttpPost]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Attempt to register the user
MembershipCreateStatus createStatus;
Membership.CreateUser(model.UserName, model.Password, model.Email, null, null, true, null, out createStatus);
if (createStatus == MembershipCreateStatus.Success)
{
FormsAuthentication.SetAuthCookie(model.UserName, false /* createPersistentCookie */);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", ErrorCodeToString(createStatus));
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
Below are some of the specific points where I need your opinion/help:
I do not necessarily want to create a new user in ASP .Net membership database.
Based on the model passed in, how do I really make sure that if the user was successfully registered or there were errors in the process.
You have a problem with your code. Your action depends on a static method: Membership.CreateUser. And as you know static methods are PITAs to unit test.
So you could weaken the coupling by introducing a level of abstraction:
public interface IMyService
{
MembershipCreateStatus CreateUser(string username, string password, string email);
}
and then have some implementation that would use the current Membership provider:
public class MyService: IMyService
{
public MembershipCreateStatus CreateUser(string username, string password, string email)
{
MembershipCreateStatus status;
Membership.CreateUser(username, password, email, null, null, true, null, out status);
return status;
}
}
and finally the controller:
public class AccountController : Controller
{
private readonly IMyService _service;
public AccountController(IMyService service)
{
_service = service;
}
[HttpPost]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Attempt to register the user
var status = _service.CreateUser(model.UserName, model.Password, model.Email);
if (status == MembershipCreateStatus.Success)
{
FormsAuthentication.SetAuthCookie(model.UserName, false /* createPersistentCookie */);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", ErrorCodeToString(createStatus));
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
}
OK, now that we have weakened the coupling we could use a mocking framework to mock the service in the unit test and make it trivial.
For example using Rhino Mocks you could create the following tests to cover the 2 failure cases:
[TestMethod]
public void Register_Action_Should_Redisplay_View_If_Model_Is_Invalid()
{
// arrange
var sut = new AccountController(null);
var model = new RegisterModel();
sut.ModelState.AddModelError("", "invalid email");
// act
var actual = sut.Register(model);
// assert
Assert.IsInstanceOfType(actual, typeof(ViewResult));
var viewResult = actual as ViewResult;
Assert.AreEqual(model, viewResult.Model);
}
[TestMethod]
public void Register_Action_Should_Redisplay_View_And_Add_Model_Error_If_Creation_Fails()
{
// arrange
var service = MockRepository.GenerateStub<IMyService>();
service
.Stub(x => x.CreateUser(null, null, null))
.IgnoreArguments()
.Return(MembershipCreateStatus.InvalidEmail);
var sut = new AccountController(service);
var model = new RegisterModel();
// act
var actual = sut.Register(model);
// assert
Assert.IsInstanceOfType(actual, typeof(ViewResult));
var viewResult = actual as ViewResult;
Assert.AreEqual(model, viewResult.Model);
Assert.IsFalse(sut.ModelState.IsValid);
}
The final test is the success case. We still have an issue with it. The issue is the following line:
FormsAuthentication.SetAuthCookie(model.UserName, false);
What is this? It is a static method call. So we proceed the same way as we did with the membership provider to weaken the coupling of our controller and the forms authentication system.
To test this method you can follow two way
Inside the test class create a new class that inherits from Membership class and override the method CreateUser.
Use Moq to mock the class.
For the first case I will check if username is equal to "GoodUser" or "BadUser" and generate a MembershipCreateStatus.Success or a different status.
For the second I will setup two method that follow the same idea as in the other method. See this link for an example
Related
I'm developing a simple Custom Role-based Web Application using ASP.Net MVC, In my login Action, I'm creating a Profile session as below:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
using (HostingEnvironment.Impersonate())
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
var employeeProfile = AccountBal.Instance.GetEmployee(loginId);
Session["Profile"] = employeeProfile;
FormsAuthentication.SetAuthCookie(model.UserName, true);
}
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", #"The user name or password provided is incorrect.");
return View(model);
}
}
And I'm checking this or using this session in all Controller Actions as below:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateOrEdit(MyModel model)
{
var employee = (Employee) Session["Profile"];
if (employee == null)
return RedirectToAction("Login", "Account");
if (ModelState.IsValid)
{
// Functionality goes here....
}
}
Is there any way I can move this piece of session checking code in a base class or centralized class? so that, I do not need to check it every time in a Controller Actions instead I will access the properties directly
say,
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateOrEdit(MyModel model)
{
var employee = _profileBase.GetCurrentProfile();
if (employee == null)
return RedirectToAction("Login", "Account");
if (ModelState.IsValid)
{
// Functionality goes here....
}
}
Create a base controller that contains your GetCurrentProfile method to retrieve current user profile like
public class BaseController : Controller
{
public Employee GetCurrentProfile()
{
return (Employee)Session["Profile"];
}
public bool SetCurrentProfile(Employee emp)
{
Session["Profile"] = emp;
return true;
}
}
And inherit your desired controller with above BaseController and access your GetCurrentProfile method like below
public class HomeController : BaseController
{
public ActionResult SetProfile()
{
var emp = new Employee { ID = 1, Name = "Abc", Mobile = "123" };
//Set your profile here
if (SetCurrentProfile(emp))
{
//Do code after set employee profile
}
return View();
}
public ActionResult GetProfile()
{
//Get your profile here
var employee = GetCurrentProfile();
return View();
}
}
GetCurrentProfile and SetCurrentProfile directly available to your desired controller because we directly inherit it from BaseController.
You may usetry/catch in above code snippet.
Try once may it help you
Writing unit tests that require database access via my CustomMembershipProvider.
edit -
public class CustomMembershipProvider : MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
using (var usersContext = new UsersContext())
{
var requiredUser = usersContext.GetUser(username, password);
var userApproved = usersContext.GetUserMem(username);
if (userApproved == null) return false;
return (requiredUser != null && userApproved.IsApproved != false);
}
}
}
[TestFixture]
public class AccountControllerTest
{
[Test]
public void ShouldNotAcceptInvalidUser()
{
// OPTION1
Mock<IMembershipService> membership = new Mock<IMembershipService>();
//OPTION2
// Mock<AccountMembershipService> membership = new Mock<AccountMembershipService>();
membership.Setup(m => m.ValidateUser(It.IsAny<string>(), It.IsAny<string>()))
.Returns(false);
var logonModel = new LoginModel() { EmailorUserName = "connorgerv", Password = "pasdsword1" };
var controller = new AccountController(membership.Object);
// Act
var result = controller.Login(logonModel,"Index") as RedirectResult;
// Assert
Assert.That(result.Url, Is.EqualTo("Index"));
Assert.False(controller.ModelState.IsValid);
Assert.That(controller.ModelState[""],
Is.EqualTo("The user name or password provided is incorrect."));
}
[Test]
public void ExampleForMockingAccountMembershipService()
{
var validUserName = "connorgerv";
var validPassword = "passwordd1";
var stubService = new Mock<CustomMembershipProvider>();
bool val = false;
stubService.Setup(x => x.ValidateUser(validUserName, validPassword)).Returns(true);
Assert.IsTrue(stubService.Object.ValidateUser(validUserName, validPassword));
}
}
public class AccountController : Controller
{
public IMembershipService MembershipService { get; set; }
public AccountController(IMembershipService service){
MembershipService=service;
}
protected override void Initialize(RequestContext requestContext)
{
if (MembershipService == null) { MembershipService = new AccountMembershipService(); }
base.Initialize(requestContext);
}
public ActionResult Index()
{
return RedirectToAction("Profile");
}
public ActionResult Login()
{
if (User.Identity.IsAuthenticated)
{
//redirect to some other page
return RedirectToAction("Index", "Home");
}
return View();
}
//
// POST: /Account/Login
[HttpPost]
public ActionResult Login(LoginModel model, string ReturnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.EmailorUserName, model.Password))
{
SetupFormsAuthTicket(model.EmailorUserName, model.RememberMe);
if (Url.IsLocalUrl(ReturnUrl) && ReturnUrl.Length > 1 && ReturnUrl.StartsWith("/")
&& !ReturnUrl.StartsWith("//") && !ReturnUrl.StartsWith("/\\"))
{
return Redirect(ReturnUrl);
}
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
// If we got this far, something failed, redisplay form
return View(model);
}
}
public class AccountMembershipService : IMembershipService
{
private readonly MembershipProvider _provider;
public AccountMembershipService()
: this(null)
{
}
public AccountMembershipService(MembershipProvider provider)
{
_provider = provider ?? Membership.Provider;
}
public virtual bool ValidateUser(string userName, string password)
{
if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password");
return _provider.ValidateUser(userName, password);
}
}
Membership in web.config of main application
<membership defaultProvider="CustomMembershipProvider">
<providers>
<clear />
<add name="CustomMembershipProvider" type="QUBBasketballMVC.Infrastructure.CustomMembershipProvider" connectionStringName="UsersContext" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />
</providers>
</membership>
public class CustomMembershipProvider : MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
using (var usersContext = new UsersContext())
{
var requiredUser = usersContext.GetUser(username, password);
var userApproved = usersContext.GetUserMem(username);
if (userApproved == null) return false;
return (requiredUser != null && userApproved.IsApproved != false);
}
}
}
What happens when I run ShouldNotAcceptInvalidUser() with Option1 uncommented I can see that MembershipService is a Mock<IMembershipService> in the AccountController but it never steps into MembershipService.ValidateUser on the login Action.
When I run with option2 uncommented the same thing happens except MembershipService is Mock<AccountMembershipService> in the accountcontroller and it hits the AccountMembership Contstructor with null parameters, which in turn sets is to SqlMembershipProvider as Membership.Provider is System.Web.Security.SqlMembershipProvider
Also ExampleForMockingAccountMembershipService() doesn't seem to hit the ValidateUsermethod at all in CustomMembershipProvider and always returns true.
Hopefully this is enough to see where i'm going wrong!! :/
Thanks for providing your code. I think I have a much better handle on what you're trying to do now.
For your ShouldNotAcceptInvalidUser test, you should definitely mock IMembershipService instead of AccountMembershipService (choose option 1 over option 2). Since your controller is your SUT, it should be the only "real" class in the test, in order to minimize the number of moving parts.
With option 1, there's no reason to expect that MembershipService.ValidateUser would step into any code. The MembershipService is a mock object - and you've explicitly told it to just always return false when that method is called. Based on the code here, and using option 1, I'd expect this test to pass.
In your other test, ExampleForMockingAccountMembershipService, you're mocking your SUT which is something you should not do. Your SUT should be the only "real" object in your test. That means all collaborating objects should be mocked, leaving the SUT to be the only object doing anything meaningful. (That way, if the test fails, you know for sure that it's because of a bug in the SUT.)
(Note: ValidateUser was always returning true here because you mocked the SUT, and explicitly told it to always return true. This is why it's never a good idea to mock your SUT - mocking changes the behavior that you're trying to test.)
Based on the code you provided, I'm guessing that the reason you mocked CustomMembershipProvider is because it doesn't fully implement its abstract base class MembershipService. If this is indeed the case, then you will need to implement the missing methods manually, instead of relying on the mocking framework to provide default implementations.
Here is what I believe you were intending this test to look like:
[Test]
public void ExampleForMockingAccountMembershipService()
{
var validUserName = "connorgerv";
var validPassword = "passwordd1";
var sut = new CustomMembershipProvider();
Assert.IsTrue(sut.ValidateUser(validUserName, validPassword));
}
Something to look out for here is the fact that CustomMembershipProvider instantiates one of its dependencies: UsersContext. In a unit test, since CustomMembershipProvider is your SUT, you'd want to mock all of its dependencies. In this situation, you could use dependency injection to pass an object responsible for creating this dependency (e.g., an IUsersContextFactory), and use a mock factory and context in your test.
If you don't want to go that route, then just be aware that your test could fail because of a bug in CustomMembershipProvider or a bug in UsersContext.
So, the general logic in your tests is sound; the problems mainly stem from confusion on the role of mock objects in your tests. It's kind of a tough concept to get at first, but here are some resources that helped me when I was learning this:
"Dependency Injection in .Net" by Mark Seemann
"Test Doubles" by Martin Fowler
Implementing a basic authorization and authentication layer is quite easy with ASP.NET MVC 4; it's all automatically generated with the 'ASP.NET MVC 4 Web Application'-project template.
However, I'm tasked with implementing some controller actions that require re-authentication and I'm aiming for a maintainable solution. Simply put in a user story, I'm trying to implement the following:
User logs on;
User navigates to a controller (attributed with [Authorize]) action which renders a form view;
User performs a POST by submitting the form;
An authentication form appears in which the user needs to re-authenticate using his/her username and password;
If authentication is succesfull, proceed with handling the POST-request.
Note that 'reauthentication' does not have to alter the state of the current user session.
Obviously, there are many ways to implementing this, but I feel like an implementation which looks similiar to the following (pseudo) sample would suit my needs.
[Authorize]
[InitializeSimpleMembership]
public class SpecialActionController : Controller
{
public ActionResult SpecialForm() { return View(); }
public ActionResult Succes() { return View(); }
[HttpPost]
[ReAuthenticate] /* <- Prompts user with reauthentication form before proceeding. */
public ActionResult SpecialForm(SpecialFormModel model)
{
if (ModelState.IsValid)
RedirectToAction("Succes");
else
return View(model);
}
}
Any suggestions?
Edit: I forgot to mention that any OAuth-related features are out of scope. External authentication is not an issue here and does not require support. In fact, with the current project I'm working on, all OAuth-related features are either removed or deactivated.
You should be able to do this using a combination of a custom AuthorizeAttribute and the Session. Override the AuthorizeCore method and let all the default authentication take place but introduce your own extra check (for re-authentication) e.g.
public class RecurringAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var reauth = (bool?)httpContext.Session["ReAuthenticated"];
var result = base.AuthorizeCore(httpContext) && (reauth ?? false);
httpContext.Session["ReAuthenticated"] = !result;
return result;
}
}
This should re-direct the user to the login page everytime they hit the action and they haven't re-authenticated. If the user has re-authenticated, we clear the session variable to force a login on the next request.
For this to work correctly, we need a hook to set the ReAuthentication session variable - I think the LogOn method in the AccountController would be the ideal place for this
public class AccountController : Controller
{
...
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe))
{
Session["ReAuthenticated"] = User.Identity.IsAuthenticated;
return RedirectToLocal(returnUrl);
}
...
}
}
Then all that's left to do is decorate our controller actions
[Authorize]
public ActionResult SomePrivateAction()
{
...
}
[RecurringAuthorize]
public ActionResult SomeSuperSecretAction()
{
...
}
You should find authorization will work as normal for any actions using the default AuthorizeAttribute and any actions decorated with the RecurringAuthorizeAttribute will be forced to login everytime they request the page, which includes page refreshes.
I tried to implement the hypothetical [ReAuthenticate]-attribute, but found myself relying on reflection too much. After putting some thought into a more manageable solution, I finally came up with the following:
ReAuth class
public sealed class ReAuth
{
#region Constructor
private ReAuth(Func<System.Web.Mvc.ActionResult> onSuccessAction)
{
this.onSuccessAction = onSuccessAction;
}
#endregion
#region Public static members
public static ReAuth CreateFor(HttpSessionStateBase httpSession, Func<System.Web.Mvc.ActionResult> onSuccessAction)
{
httpSession[sessionKey] = new ReAuth(onSuccessAction);
return GetFor(httpSession);
}
public static ReAuth GetFor(HttpSessionStateBase httpSession)
{
return httpSession[sessionKey] as ReAuth;
}
public static bool ExistsFor(HttpSessionStateBase httpSession)
{
return httpSession[sessionKey] as ReAuth != null;
}
#endregion
#region Public instance members
public bool ReAuthenticated { get; set; }
public System.Web.Mvc.ActionResult Handle()
{
if (ReAuthenticated)
return onSuccessAction();
else
return new System.Web.Mvc.RedirectToRouteResult(
new System.Web.Routing.RouteValueDictionary
{
{ "Controller", "#" }, /* Replace '#' with the name of the controller that implements the re-authentication form... */
{ "Action", "#" } /* Replace '#' with the name of the action on the aforementioned controller. */
});
}
#endregion
#region Private members
private const string sessionKey = "reAuthenticationSessionKey";
private readonly Func<System.Web.Mvc.ActionResult> onSuccessAction;
#endregion
}
Implementation
Suppose we have a hypothetical controller where the solution is applied:
public class AccountInfoController : System.Web.Mvc.Controller
{
/* snip... */
[HttpPost]
public ActionResult EditAccountInfo(AccountInfo model)
{
if (ModelState.IsValid)
return ReAuth.CreateFor(Session, () => { return Success(); }).Handle();
else
return View(model);
}
}
...and, we need a controller (essentially, a 'dumb' copy of the real AccountController that does not tamper with the Forms Authentication User Session state) in which the re-authentication takes place.:
public class ReAuthController : System.Web.Mvc.Controller
{
/* Snip... */
[HttpPost]
public ActionResult LogOn(LogOnModel model)
{
if (ModelState.IsValid)
{
ReAuth.GetFor(Session).ReAuthenticated = Membership.ValidateUser(model.User, model.Password);
return ReAuth.Handle();
}
return View(model);
}
}
As far as I know, this is a manageable solution. It does rely a lot on storing objects into session state. (Especially the object state of the controller which implements the ReAuth-class) If anyone has additional suggestions, please let me know!
I don't want to repeat myself. That is, I don't want the same code in two different controllers.
I always start from a default mvc5 web app project. That project has a Register ActionMethod in an AccountController:
//
// GET: /Account/Register
[AllowAnonymous]
public ActionResult Register()
{
return View();
}
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser() { UserName = model.UserName };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", "Home");
}
else
{
AddErrors(result);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
Say I have a CampaignController and I want to register a user when he/she is on that page, fills out his/her username and pass and clicks the send form/submit button. What is the best thing to do in the ActionMethod of that form/controller?
Yes, I want to have the registerform in two or more places.
What is the best way to accomplish this in mvc 5?
Create a logic layer
put the register logic in that logic layer
add the logic layer as reference in the web layer
hit the function in the logic layer from both actionresults :)
In your example at least... I would call the actions of your AccountController from your Campaign views.
For me it working like a charm after setting the current ControllerContext to AccountControllerContext
//This is employee controller class
public ActionResult Create([Bind(Include = "EmployeeId,FirstName,LastName,DOJ,DOB,Address,City,State,Mobile,Landline,ReportsTo,Salary")] Employee employee)
{
if (ModelState.IsValid)
{
AccountController accountController = new AccountController();
accountController.ControllerContext = this.ControllerContext;
//accountController.UserManager;
var userId = accountController.RegisterAccount(new RegisterViewModel { Email = "temp#temp.com", Password = "Pravallika!23" });
if (!string.IsNullOrEmpty(userId))
{
employee.UserId = userId;
employee.CreatedBy = User.Identity.GetUserId();
db.Employees.Add(employee);
db.SaveChanges();
return RedirectToAction("Index");
}
}
//customized method in AccountController
public string RegisterAccount(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = UserManager.Create(user, model.Password);
//to add roles
//UserManager.AddToRole(user.Id, "Admin");
if (result.Succeeded)
{
return user.Id;
}
else
{
AddErrors(result);
}
}
// If we got this far, something failed
return null;
}
So I am currently using a ViewBag set in the login to determine if they can see admin only stuff. This is done this way because Roles.CreateRole, Membership.CreateUser and Roles.AddUserToRole is disabled because we use ModelFirst ASP.net.
public ActionResult Login(LoginModel model, string returnUrl)
{
ViewBag.Admin = false;
if (model.IsValid(model.UserName, model.Password))
{
ViewBag.Admin = (bool)model.currentLoggedInEmployee.IsAdmin;
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", "Login data is incorrect!");
return View(model);
}
}
Then we simply use:
#if (ViewBag.Admin == true) {
<li>#Html.ActionLink("Administration", "Index", "Administration")</li>
}
to only show these buttons to admins. This works.
Now what we want, is to make sure only administrators can run some functions, by doing something similar to the normal
[Authenticate(Roles="Admin")]
[HttpPost]
public ActionResult Create(FormCollection collection)
{
// TODO: Add insert logic here
}
But because we don't have any "Roles" we can not do it like this. We need to use the ViewBag.Admin value to authorize people to use these functions. Question is, how can this be done?
I would recommend rolling your own AuthorizeAttribute and from there you can determine whether or not the current logged in user is an admin or not.
When you create your authentication cookie add some additional information (i.e. the admin flag) e.g.
public ActionResult Login(LoginModel model, string returnUrl)
{
if (model.IsValid(model.UserName, model.Password))
{
var ticket = new FormsAuthenticationTicket(1,
model.UserName,
DateTime.Now,
DateTime.Now.AddMinutes(30),
model.RememberMe,
model.currentLoggedInEmployee.IsAdmin, // user data
FormsAuthentication.FormsCookiePath);
// Encrypt the ticket.
string encTicket = FormsAuthentication.Encrypt(ticket);
// Create the cookie.
Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
// Redirect back to original URL.
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", "Login data is incorrect!");
return View(model);
}
}
Create a custom authorize attribute to authenticate the logged in user against the role e.g.
public class AdminOnlyAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext.Current.User.Identity.IsAuthenticated)
{
var ticket = ((FormsIdentity)User.Identity).Ticket;
return (bool)ticket.UserData;
}
else
{
return false;
}
}
}
Then decorate your action as:
[AdminOnly]
[HttpPost]
public ActionResult Create(FormCollection collection)
{
// TODO: add insert logic here
}