lundi 1 juillet 2019

Mocked UserManager doesn't really work as expected in tests

I have some service that's responsible for changing user password while using UserManager and everything works fine, but when I want to write some tests, then it starts failing at method CheckPassword, which basically checks if current (old password) is correct

myService:
public async bool TryChangePassword(User user, string OldPassword, string NewPassword)
{
    (...)

    // it returns false
    var checkOldPassword = await _userManager.CheckPasswordAsync(user, OldPassword);

    if (!checkOldPassword)
    {
        return false;
    }

    var token = await _userManager.GeneratePasswordResetTokenAsync(user);

    var result = await _userManager.ResetPasswordAsync(user, token, NewPassword);

    return result.Succeeded;
}

myTests:

private readonly UserManager<User> _userManager;

[Fact]
public void password_change_attempt_1()
{
    var service = new myService(_context, _userManager);

    var user = new User("john");

    var register = _userManager.CreateAsync(user, "123456");

    _context.SaveChanges();

    Assert.True(_context.Users.Any());

    var result = service.TryChangePassword(user, "123456", "newPassword");
}

but for some reason it fails at:

var checkOldPassword = await _userManager.CheckPasswordAsync(user, OldPassword);

it returns false, but as you can see, provided password is correct, probably there's something wrong with mocking user manager

Here's how do I create mock of UserManager in Tests' constructor

public Tests()
{
    var o = new DbContextOptionsBuilder<Context>();
    o.UseInMemoryDatabase(Guid.NewGuid().ToString());
    _context = new Context(o.Options);
    _context.Database.EnsureCreated();

    var userStore = new MockUserStore(_context);
    _userManager = new MockUserManager(userStore,
                        new Mock<IOptions<IdentityOptions>>().Object,
                        new Mock<IPasswordHasher<User>>().Object,
                        new IUserValidator<User>[0],
                        new IPasswordValidator<User>[0],
                        new Mock<ILookupNormalizer>().Object,
                        new Mock<IdentityErrorDescriber>().Object,
                        new Mock<IServiceProvider>().Object,
                        new Mock<ILogger<UserManager<User>>>().Object);
}

public class MockUserManager : UserManager<User>
{
    public MockUserManager(IUserStore<User> store, IOptions<IdentityOptions> optionsAccessor,
     IPasswordHasher<User> passwordHasher, IEnumerable<IUserValidator<User>> userValidators,
      IEnumerable<IPasswordValidator<User>> passwordValidators, ILookupNormalizer keyNormalizer,
       IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<User>> logger)
        : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
    {
    }

    public override Task<IdentityResult> CreateAsync(User user)
    {
        this.Store.CreateAsync(user, new CancellationToken());
        return Task.FromResult(IdentityResult.Success);
    }
}
public class MockUserStore : IUserStore<User>, IUserPasswordStore<User>
{
    public readonly Context _ctx;

    public MockUserStore(Context ctx)
    {
        _ctx = ctx;
    }
    public Task<IdentityResult> CreateAsync(User user, CancellationToken cancellationToken)
    {
        _ctx.Users.Add(user);

        return Task.FromResult(IdentityResult.Success);
    }

    public Task<IdentityResult> DeleteAsync(User user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        throw new NotImplementedException();
    }

    public Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<string> GetNormalizedUserNameAsync(User user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<string> GetPasswordHashAsync(User user, CancellationToken cancellationToken)
    {
        return Task.FromResult<string>(user.PasswordHash);
    }

    public Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken)
    {
        return Task.FromResult<string>(user.Id);
    }

    public Task<string> GetUserNameAsync(User user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<bool> HasPasswordAsync(User user, CancellationToken cancellationToken)
    {
        return Task.FromResult<bool>(!String.IsNullOrEmpty(user.PasswordHash));
    }

    public Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task SetPasswordHashAsync(User user, string passwordHash, CancellationToken cancellationToken)
    {
        user.PasswordHash = passwordHash;
        return Task.FromResult(0);
    }

    public Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<IdentityResult> UpdateAsync(User user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

I suppose it can be fixed here:

public Task<IdentityResult> CreateAsync(User user, CancellationToken cancellationToken)
{
    _ctx.Users.Add(user);

    return Task.FromResult(IdentityResult.Success);
}

by adding something like

user.PasswordHash = generateHash(password)

but how can I know how many iterations do ASP.NET Core Identity does use?

https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing?view=aspnetcore-2.2

string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
    password: password,
    salt: salt,
    prf: KeyDerivationPrf.HMACSHA1,
    iterationCount: 10000,
    numBytesRequested: 256 / 8));

Aucun commentaire:

Enregistrer un commentaire