vendredi 1 juillet 2016

Issues Unit Testing EF & ASP.NET Identity - various exceptions from EF & FakeItEasy

Background:

I am working on getting some experience in unit testing, as per my new employer's strict unit testing requirements, but unit testing as a whole is new to me. I have had a TON of issues trying to test any of the methods that make use of ASP.NET Identity, due to the reliance on HttpContext.Current.GetOwinContext().Authentication and HttpContext.Current.GetOwinContext().GetUserManager

Now, this current project has not fully taken advantage of Interfaces and Dependency Injection so far, but as a part of incorporating our organization's Active Directory system into our Identity Database (creating a row in the Users table for someone when they log in with valid Active Directory credentials, and then using the Identity database for them from then on), I have been working on retrofitting our Identity side of things with Interfaces and Ninject, and adding FakeItEasy to our test unit test projects.

This does mean that we are currently generating tests that use the actual databases themselves as opposed to faking things. We know this is a bad practice and it was done simply for the sake of not overloading us new guys' minds while we work on our first real project. We have worked out (most/all) of the kinks this causes and our tests clean things up when they are finished.

Question 1:
I am running into a peculiar issue while trying to unit test the following method:

public bool ResetPassword(User user)
    {
        if (!user.EmailConfirmed) return false;

        user.RequirePasswordReset = true;
        string randomGeneratedPassword = GenerateRandomPassword(20); // defined at end of class
        user.PasswordHash = hash.HashPassword(randomGeneratedPassword);

        if (!UpdateUser(user)) return false;

        string message = "We have received a request to reset your password. <br /><br />" +
            "Your new password is shown below. <br /><br />" +
            $"Your new password is: <br />{randomGeneratedPassword}<br /><br />" +
            "This password is only valid for one login, and must be changed once it is used. <br /><br /><br /><br />" +
            "Server<br />AmTrust Developer University";

        SendFormattedEmail(user.Email, user.FullName, message, "Your password has been reset");
        return true;
    }

The test I have written so far to do so (with the testcases omitted) is:

public bool ResetPasswordTests(bool emailConfirmed)
    {
        //arrange
        _user = new User()
        {
            Email = "ttestingly@test.com",
            EmailConfirmed = emailConfirmed,
            FirstName = "Test",
            isActive = true,
            isActiveDirectoryAccount = false,
            LastName = "Testingly",
            PasswordHash = _hash.HashPassword("secret1$"),
            RequirePasswordReset = false,
            UserName = "ttestingly"
        };

        string hashedPass = _user.PasswordHash;

        _identityContext.Users.Add(_user);
        _identityContext.SaveChanges();

        //Suppress the email sending bit!
        A.CallTo(() => _userBusinessLogic_Testable.SendFormattedEmail(null, null, null, null, null))
        .WithAnyArguments()
        .DoesNothing();

        //act
        bool result = _userBusinessLogic_Testable.ResetPassword(_user);

        //assert
        Assert.That(result);
        Assert.That(_user.PasswordHash != hashedPass);
        Assert.That(_user.RequirePasswordReset);
        return result;
    }

Running this test (for all of its various TestCases) returns the following exception:

System.NotSupportedException: Model compatibility cannot be checked because the database does not contain model metadata. Model compatibility can only be checked for databases created using Code First or Code First Migrations.

This is caused by _identityContext.Users.Add(_user);

Everything I've seen about this issue indicates that it is caused by an open connection to the database while code is run trying to connect to that database, which I don't think is the case, or from trying to have EF manage a pre-existing database (which is not the case: I have deleted my databases multiple times between tests to try to verify this).

Note: Currently all of our team's databases are just localhost databases, so there's no one else messing with my stuff.

I have seen an example of this where the solution was to change the connection string, however this issue ONLY happens in Unit Testing - I have verified that while running, everything on the application works as expected prior to any changes I have made to incorporate Interfaces, Ninject, and Active Directory - so I do not think the connection string itself is the issue, but here is the relevant connection string (they are proper XML but I'm not sure how to get them to show up properly on Stack Overflow, so I removed all of the braces):

connectionStrings
add name="ADUUserDB" providerName="System.Data.SqlClient" connectionString="Data Source=localhost\sql2014;Initial Catalog=ADUUserDB;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False; MultipleActiveResultSets=True"
/connectionStrings

Question 2:
On the other side of the application, I am attempting to test one of my Controllers (which are currently not unit tested at all, due to not having FakeItEasy or any alternative prior to now), and all of my attempted tests are throwing the following exception:

FakeItEasy.Configuration.FakeConfigurationException:
The current proxy generator can not intercept the specified method for the following reason:
- Extension methods can not be intercepted since they're static.

This occurs at the very first attempted A.CallTo() I have written for the following test:

    public async Task LoginTests(bool activeDirectory, string returnUrl, bool emailConfirmed, bool requirePasswordReset)
    {
        if (activeDirectory) requirePasswordReset = false;
        //arrange
        _user = new User()
        {
            Email = activeDirectory ? "99999" : "ttestingly@test.com",
            EmailConfirmed = emailConfirmed,
            FirstName = "Test",
            isActive = true,
            isActiveDirectoryAccount = activeDirectory,
            LastName = "Testingly",
            RequirePasswordReset = requirePasswordReset,
            PasswordHash = _hash.HashPassword("secret1$"),
            UserName = "ttestingly"
        };

        if (!activeDirectory)
        {
            A.CallTo(() => _userBusinessLogic.GetUsers(null, null, null, null, null, null, null)
                .Single())
                .WithAnyArguments()
                .Returns(_user);
        }
        else
        {
            A.CallTo(() => _userBusinessLogic.GetUsers(null, null, null, null, null, null, null)
                .Single())
                .WithAnyArguments()
                .Throws(new ApplicationException());

            A.CallTo(() => _userBusinessLogic.CreateActiveDirectoryAccount(99999, "secret1$", false))
                .Returns(true);
        }

        A.CallTo(() => _userBusinessLogic.CreateClaimsIdentityForUser(null, false))
            .WithAnyArguments()
            .Returns(true);

        LoginViewModel model = new LoginViewModel()
        {
            UserName = _user.UserName,
            Password = "secret1$",
            RememberMe = false,
        };

        //act
        ActionResult result = await _controller.Login(model, returnUrl);

        //assert
        if (returnUrl != null) Assert.That(result is RedirectResult);

        else if (activeDirectory || emailConfirmed) Assert.That(result is ViewResult);

        else if (requirePasswordReset) Assert.That(result is RedirectToRouteResult);
    }

The variable that we are calling the method from is private IUserBusinessLogic _userBusinessLogic = A.Fake<IUserBusinessLogic>();

As you can see, it is a method signature that is defined as part of an Interface, shown below:

public interface IUserBusinessLogic
{
    HttpContext CurrentContext { get; }

    bool ChangePassword(string userId, string currentPassword, string newPassword, out List<string> errors);

    Task<bool?> CreateActiveDirectoryAccount(uint adUserName, string password, bool RememberMe = false);
    Task<bool> CreateClaimsIdentityForUser(User user, bool rememberMe = false);
    Task<bool> CreateIdentityAsync(User user, string authenticationType, bool rememberMe = false);

    IList<DbValidationError> CreateUser(User user, bool privateExamTaken, bool privateExamDeadline, bool publicExamTaken, bool takenExamGraded, bool takenExamDeadline);
    User FindOrGenerateUser(string Email, string FirstName, string LastName);
    IList<User> GetUsers(IList<string> UserIds = null, IList<long> StudentIds = null, 
        string firstNameText = null, string lastNameText = null, 
        string userNameText = null, string emailText = null, 
        bool? isActive = default(bool?));

    void Logout();
    void SendEmail(string message, string userEmail, string subject = "Password Reset Request", string replyTo = null);
    void SendFormattedEmail(string Email, string FullName, string Message, string Subject, string replyTo = null);
    void SendNotificationEmail(AmtrustDeveloperUniversityUser student, NotificationOptions notification, ExamSchedule schedule);

    Task<bool> SyncUserPassword_With_ActiveDirectory(User user, string Password, bool RememberMe = false);

    bool ResetPassword(User user);
    bool UpdateUser(User user);

    ApplicationException UpdateUserActiveStatus(string userId, bool activeStatus);
    Task<IdentityResult> ValidateAsync(string password);

    bool ValidateActiveDirectoryCredentials(uint username, string password, out UserPrincipal principal);
}

I have done some reading and found that Fakes can only be made of Virtual method calls, but given that I am faking an interface, it should be the case that this issue never crops up - if it were only the .CreateClaimsIdentityForUser() call, I could at least understand why this was happening (as that method call in the injected class uses UserManager.CreateIdentityAsync) - but faking this and telling it what I want it to return shouldn't be an issue, I would think.

The ninject bindings I have defined so far are: private void AddBindings(IKernel kernel) { kernel.Bind().To(); kernel.Bind().To(); kernel.Bind().To(); }

...I've run out of characters to use so if you need more information just ask. Any help is appreciated, be it something I need to go learn about, clarification for what is meant by these errors, or a better method of testing the code I am trying to test.

Aucun commentaire:

Enregistrer un commentaire