Знущання над 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!
Фелікс,

Після незліченних годин пошуку це було повідомлення, яке нарешті привело мене до квадрата. У моєму основному методі контролера проектів 2.0 я використовував User.FindFirstValue(ClaimTypes.NameIdentifier);для встановлення userId на об'єкті, який я створював, і зазнав невдачі, оскільки принципал був нульовим. Це виправило це для мене. Дякуємо за чудову відповідь!
Тімоті Рендалл

Я також шукав незліченну кількість годин, щоб запустити UserManager.GetUserAsync, і це єдине місце, де я знайшов відсутні посилання. Дякую! Потрібно встановити ClaimsIdentity, що містить претензію, а не використовувати 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властивість дозволяє встановити його значення, щоб це було вашим шляхом.

Отже, метою є досягнення цього об’єкта. В 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);
}

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і бізнес - логіка сидить поза контролера. Я зміг використати для цього комбінацію відповідей Нкосі, Каліна та Поке:

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, буде щось "нестандартне". Але, мабуть, ні!
Фелікс

Крім того, користувач є змінною члена ControllerBase. Ось чому в попередніх версіях ASP.NET люди висміювали HttpContext і отримували звідти IPrincipal. Не можна просто отримати Користувача з автономного класу, такого як 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;
    ..................
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.