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);
}
но это также породило ошибку.
Не удалось найти ничего в документах...
Ответы
Ответ 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