Мокинг IPrincipal в ASP.NET Core

89

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

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

что явно не проходит в модульном тесте. Я огляделся, и все предложения от .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);
}

но это тоже привело к ошибке. В документации тоже ничего не нашел ...

Феликс
источник

Ответы:

179

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

Самый простой способ задать пользователя - назначить другой 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, не забудьте передать конструктору явное authenticationTypeзначение. Это гарантирует, что это IsAuthenticatedбудет работать правильно (если вы используете это в своем коде, чтобы определить, аутентифицирован ли пользователь).

тыкать
источник
7
В моем случае это должно new Claim(ClaimTypes.Name, "1")было соответствовать использованию контроллера user.Identity.Name; но в остальном это именно то, чего я пытался достичь ... Danke schon!
Felix
После бесчисленных часов поисков это был тот пост, который наконец заставил меня задуматься. В моем методе контроллера проекта ядра 2.0 я использовал, User.FindFirstValue(ClaimTypes.NameIdentifier);чтобы установить userId для объекта, который я создавал, и не работал, потому что принципал был нулевым. Это исправило это для меня. Спасибо за отличный ответ!
Тимоти Рэндалл
Я также бесчисленное количество часов искал, чтобы заставить UserManager.GetUserAsync работать, и это единственное место, где я нашел недостающую ссылку. Благодарность! Необходимо настроить ClaimsIdentity, содержащий Claim, а не использовать GenericIdentity.
Этьен
17

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

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

/// <summary>
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
public ClaimsPrincipal User => HttpContext?.User;

и контроллер получает доступ к переходному HttpContextотверстиюControllerContext

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

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

Итак, цель - добраться до этого объекта. In 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<ClaimsPrincipal>();
    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);
}
Nkosi
источник
3

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

var user = new Mock<ClaimsPrincipal>();
_controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext
    {
        User = user.Object
    }
};
Калин
источник
3

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

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();
Люк
источник
2

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

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

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

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

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());
Джеймс Вуд
источник
Спасибо. Я делаю нечто подобное для своих объектов. Я просто надеялся, что для такой обычной вещи, как IPrinicpal, будет что-то «нестандартное». Но, видимо, нет!
Felix
Кроме того, пользователь является переменной-членом ControllerBase. Вот почему в более ранних версиях ASP.NET люди издевались над HttpContext и получали оттуда IPrincipal. Нельзя просто получить User из отдельного класса, такого как ProductionFactory,
Феликс
1

Я хочу напрямую поразить свои контроллеры и просто использовать DI, например AutoFac. Для этого я сначала регистрируюсь ContextController.

var identity = new GenericIdentity("Test User");
var httpContext = new DefaultHttpContext()
{
    User = new GenericPrincipal(identity, null)
};

var context = new ControllerContext { HttpContext = httpContext};
builder.RegisterInstance(context);

Затем я включаю внедрение свойств при регистрации контроллеров.

  builder.RegisterAssemblyTypes(assembly)
                    .Where(t => t.Name.EndsWith("Controller")).PropertiesAutowired();

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

public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
    var requestedBy = User.Identity?.Name;
    ..................
Devgig
источник