Подтвердить что ты не робот

Mocking IPrincipal в ядре ASP.NET

У меня есть приложение ASP.NET MVC Core, для которого я пишу модульные тесты. Один из методов действия использует имя пользователя для некоторых функций:

SettingsViewModel svm = _context.MySettings(User.Identity.Name);

который, очевидно, терпит неудачу в unit test. Я посмотрел вокруг, и все предложения от .NET 4.5, чтобы высмеять HttpContext. Я уверен, что есть лучший способ сделать это. Я попытался ввести IPrincipal, но он сделал ошибку; и я даже попробовал это (из отчаяния, я полагаю):

public IActionResult Index(IPrincipal principal = null) {
    IPrincipal user = principal ?? User;
    SettingsViewModel svm = _context.MySettings(user.Identity.Name);
    return View(svm);
}

но это также породило ошибку. Не удалось найти ничего в документах...

4b9b3361

Ответ 1

Доступ к контроллеру User осуществляется через HttpContext контроллера. Последний хранится в ControllerContext.

Самый простой способ установить пользователя - назначить другой HttpContext для созданного пользователя. Мы можем использовать DefaultHttpContext для этой цели, так что нам не нужно все высмеивать. Затем мы просто используем этот HttpContext в контексте контроллера и передаем его экземпляру контроллера:

var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
    new Claim(ClaimTypes.Name, "example name"),
    new Claim(ClaimTypes.NameIdentifier, "1"),
    new Claim("custom-claim", "example claim value"),
}, "mock"));

var controller = new SomeController(dependencies…);
controller.ControllerContext = new ControllerContext()
{
    HttpContext = new DefaultHttpContext() { User = user }
};

При создании вашего собственного ClaimsIdentity обязательно передайте явный ClaimsIdentity authenticationType конструктору. Это гарантирует, что IsAuthenticated будет работать правильно (если вы используете это в своем коде, чтобы определить, аутентифицирован ли пользователь).

Ответ 2

В предыдущих версиях вы могли бы установить User непосредственно на контроллер, что сделало для очень простых модульных тестов.

Если вы посмотрите на исходный код для ControllerBase, вы заметите, что User извлекается из HttpContext.

/// <summary>
/// Gets or sets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
public ClaimsPrincipal User
{
    get
    {
        return HttpContext?.User;
    }
}

и контроллер обращается к HttpContext через ControllerContext

/// <summary>
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
/// </summary>
public HttpContext HttpContext
{
    get
    {
        return ControllerContext.HttpContext;
    }
}

Вы заметите, что эти два являются свойствами только для чтения. Хорошей новостью является то, что свойство ControllerContext позволяет установить его значение так, чтобы оно было вашим.

Итак, цель - добраться до этого объекта. В Core HttpContext является абстрактным, поэтому его намного проще высмеять.

Предполагая контроллер, например

public class MyController : Controller {
    IMyContext _context;

    public MyController(IMyContext context) {
        _context = context;
    }

    public IActionResult Index() {
        SettingsViewModel svm = _context.MySettings(User.Identity.Name);
        return View(svm);
    }

    //...other code removed for brevity 
}

Используя Moq, тест может выглядеть так:

public void Given_User_Index_Should_Return_ViewResult_With_Model() {
    //Arrange 
    var username = "FakeUserName";
    var identity = new GenericIdentity(username, "");

    var mockPrincipal = new Mock<IPrincipal>();
    mockPrincipal.Setup(x => x.Identity).Returns(identity);
    mockPrincipal.Setup(x => x.IsInRole(It.IsAny<string>())).Returns(true);

    var mockHttpContext = new Mock<HttpContext>();
    mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);

    var model = new SettingsViewModel() {
        //...other code removed for brevity
    };

    var mockContext = new Mock<IMyContext>();
    mockContext.Setup(m => m.MySettings(username)).Returns(model);

    var controller = new MyController(mockContext.Object) {
        ControllerContext = new ControllerContext {
            HttpContext = mockHttpContext.Object
        }
    };

    //Act
    var viewResult = controller.Index() as ViewResult;

    //Assert
    Assert.IsNotNull(viewResult);
    Assert.IsNotNull(viewResult.Model);
    Assert.AreEqual(model, viewResult.Model);
}

Ответ 3

Я бы хотел реализовать абстрактный шаблон Factory.

Создайте интерфейс для Factory специально для предоставления имен пользователей.

Затем укажите конкретные классы, которые предоставляют User.Identity.Name, и тот, который предоставляет другое твердое кодированное значение, которое работает для ваших тестов.

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

interface IUserNameFactory
{
    string BuildUserName();
}

class ProductionFactory : IUserNameFactory
{
    public BuildUserName() { return User.Identity.Name; }
}

class MockFactory : IUserNameFactory
{
    public BuildUserName() { return "James"; }
}

IUserNameFactory factory;

if(inProductionMode)
{
    factory = new ProductionFactory();
}
else
{
    factory = new MockFactory();
}

SettingsViewModel svm = _context.MySettings(factory.BuildUserName());

Ответ 4

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

var user = new Mock<ClaimsPrincipal>();
_controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext
    {
        User = user.Object
    }
};

Ответ 5

В моем случае мне нужно было использовать Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Name и некоторую бизнес-логику, Request.HttpContext.User.Identity.Name вне контроллера. Я смог использовать комбинацию ответов Нкоси, Калина и Поке для этого:

var identity = new Mock<IIdentity>();
identity.SetupGet(i => i.IsAuthenticated).Returns(true);
identity.SetupGet(i => i.Name).Returns("FakeUserName");

var mockPrincipal = new Mock<ClaimsPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(identity.Object);

var mockAuthHandler = new Mock<ICustomAuthorizationHandler>();
mockAuthHandler.Setup(x => x.CustomAuth(It.IsAny<ClaimsPrincipal>(), ...)).Returns(true).Verifiable();

var controller = new MyController(...);

var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);

controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = new DefaultHttpContext()
{
    User = mockPrincipal.Object
};

var result = controller.Get() as OkObjectResult;
//Assert results

mockAuthHandler.Verify();

Ответ 6

Вы можете смоделировать HttpContext в Net Core, используя IHttpContextAccessor - примерно так:

public class UserRepository : IUserRepository
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserRepository(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void LogCurrentUser()
    {
        var username = _httpContextAccessor.HttpContext.User.Identity.Name;
        service.LogAccessRequest(username);
    }
}

Это взято с этой страницы: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-2.2