繁体   English   中英

ASP.NET 内核中的 Mocking IPrincipal

[英]Mocking IPrincipal in ASP.NET Core

我有一个 ASP.NET MVC Core 应用程序,我正在为其编写单元测试。 其中一种操作方法将用户名用于某些功能:

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

这显然在单元测试中失败了。 我环顾四周,所有建议都来自 .NET 4.5 到 mock HttpContext。 我确信有更好的方法来做到这一点。 我试图注入 IPrincipal,但它抛出了一个错误; 我什至尝试过这个(我想是出于绝望):

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

但这也引发了错误。 在文档中也找不到任何内容...

控制器的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 ,请确保将显式authenticationType传递给构造函数。 这可确保IsAuthenticated正常工作(以防您在代码中使用它来确定用户是否已通过身份验证)。

在以前的版本中,您可以直接在控制器上设置User ,这样可以进行一些非常简单的单元测试。

如果您查看ControllerBase的源代码,您会注意到User是从HttpContext提取的。

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

并且控制器通过ControllerContext访问HttpContext

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

您会注意到这两个是只读属性。 好消息是ControllerContext属性允许设置它的值,以便您进入。

所以目标是到达那个对象。 在核心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);
}

还可以使用现有的类,并仅在需要时进行模拟。

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

就我而言,我需要使用Request.HttpContext.User.Identity.IsAuthenticatedRequest.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();

我想直接点击我的控制器,然后像 AutoFac 一样使用 DI。 为此,我首先注册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;
    ..................

我想实现一个抽象工厂模式。

为工厂创建一个接口,专门用于提供用户名。

然后提供具体的类,一个提供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());

我有一个棕地 .net 4.8 项目,我需要将其转换为 .net 5.0,我希望尽可能多地保留原始代码,包括单元\/集成测试。 控制器的测试非常依赖于上下文,所以我创建了这个扩展方法来启用设置令牌、声明和标题:

public static void AddContextMock(
    this ControllerBase controller,
    IEnumerable<(string key, string value)> claims = null,
    IEnumerable<(string key, string value)> tokens = null,
    IEnumerable<(string key, string value)> headers = null)
{
    HttpContext mockContext = new DefaultHttpContext();
    if(claims != null)
    {
        mockContext.User = SetupClaims(claims);
    }
    if(tokens != null)
    {
        mockContext.RequestServices = SetupTokens(tokens);
    }
    if(headers != null)
    {
        SetupHeaders(mockContext, headers);
    }

    controller.ControllerContext = new ControllerContext()
    {
        HttpContext = mockContext
    };
}

private static void SetupHeaders(HttpContext mockContext, IEnumerable<(string key, string value)> headers)
{
    foreach(var header in headers)
    {
        mockContext.Request.Headers.Add(header.key, header.value);
    }
}

private static ClaimsPrincipal SetupClaims(IEnumerable<(string key, string value)> claimValues)
{
    var claims = claimValues.Select(c => new Claim(c.key, c.value));
    return new ClaimsPrincipal(new ClaimsIdentity(claims, "mock"));
}

private static IServiceProvider SetupTokens(IEnumerable<(string key, string value)> tokenValues)
{
    var mockServiceProvider = new Mock<IServiceProvider>();
    var authenticationServiceMock = new Mock<IAuthenticationService>();
    var authResult = AuthenticateResult.Success(
        new AuthenticationTicket(new ClaimsPrincipal(), null));
    var tokens = tokenValues.Select(t => new AuthenticationToken { Name = t.key, Value = t.value });
    authResult.Properties.StoreTokens(tokens);

    authenticationServiceMock
        .Setup(x => x.AuthenticateAsync(It.IsAny<HttpContext>(), null))
        .ReturnsAsync(authResult);

    mockServiceProvider.Setup(_ => _.GetService(typeof(IAuthenticationService))).Returns(authenticationServiceMock.Object);
    return mockServiceProvider.Object;
}

如果您使用 Razor 页面并想要覆盖声明:

var user = new ClaimsPrincipal(new ClaimsIdentity(
    new Claim[] { 
        new("dateofbirth", "2000-10-10"),
        new("surname", "Smith") },
    "mock"));

_razorModel = new RazorModel()
{
    PageContext = new PageContext
    {
        HttpContext = new DefaultHttpContext() { User = user }
    }
};

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM