Mocked UserManager не работает должным образом в тестах - PullRequest
0 голосов
/ 01 июля 2019

У меня есть служба, которая отвечает за изменение пароля пользователя при использовании UserManager, и все работает нормально, но когда я хочу написать несколько тестов, происходит сбой при методе CheckPassword, который в основном проверяет, является ли current (old password) правильный

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");
}

, но по какой-то причине он не работает по адресу:

var checkOldPassword = await _userManager.CheckPasswordAsync (пользователь, OldPassword);

возвращает false, но, как вы видите, при условии, что пароль верен, возможно, что-то не так с насмешливым менеджером пользователя

Вот как мне создать макет UserManager в Tests' конструкторе

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();
    }
}

Полагаю, это можно исправить здесь:

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

    return Task.FromResult(IdentityResult.Success);
}

добавив что-то вроде

user.PasswordHash = generateHash(password)

но как я могу узнать, сколько итераций использует ASP.NET Core Identity?

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));

1 Ответ

0 голосов
/ 01 июля 2019

Вы всегда должны стараться избегать таких глубоких издевательств. Скрыть конкретную реализацию с помощью собственного интерфейса.

public interface IMyUserManager
{
    Task<bool> CheckPasswordAsync(User user, string oldPassword);
    Task<string> GeneratePasswordResetTokenAsync(User user);        
    Task<IdentityResult> ResetPasswordAsync(User user, string token, string 
        newPassword);
    }
}

Во-вторых, реализуйте этот интерфейс со встроенным UserManager

public class MyUserManager : IMyUserManager 
{
    private readonly UserManager<User> _userManager;

    public MyUserManager(UserManager<User> userManager)
    {
        if (userManager is null)
        {
           throw new ArgumentNullException(nameof(userManager));
        }
        _userManager = userManager;
    }

    public Task<bool> CheckPasswordAsync(User user, string oldPassword)
    {
        return _userManager.CheckPasswordAsync(user, oldPassword);
    }

    public Task<string> GeneratePasswordResetTokenAsync(User user)
    {
        return _userManager.GeneratePasswordResetTokenAsync(user);
    }

    public Task<IdentityResult> ResetPasswordAsync(User user, string token, 
        string newPassword)
    {
        return _userManager.ResetPasswordAsync(user, token, newPassword);
    }
}

Затем перепишите свой сервис

        private readonly IMyUserManager _userManager;

        public MyService(IMyUserManager userManager)
        {
            _userManager = userManager;
        }
        public async Task<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;

Наконец, напишите контрольные примеры как можно проще. Люди вокруг вас никогда не захотят переходить к реализации подклассов, когда исправляют тесты

[Fact]
        public async Task password_change_attempt_1()
        {
            var mock = new Mock<IMyUserManager>();
            MyService myService = new MyService(mock.Object);
            mock.Setup(x => x.CheckPasswordAsync(It.IsAny<User>(), It.IsAny<string>()))
                .Returns(Task.FromResult(true));
            mock.Setup(x => x.ResetPasswordAsync(It.IsAny<User>(), It.IsAny<string>(),
                It.IsAny<string>()))
                .Returns(Task.FromResult(IdentityResult.Success));

            var result =
                await myService
                    .TryChangePassword(new User { Name = "Name", }, "OldPassword", "NewPassword");

            Assert.Equal(result, true);
        }
...