在ASP.NET Core中模拟IPrincipal


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);
}

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

Answers:


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,但由于principal为空而失败。这为我解决了这个问题。感谢您的出色回答!
蒂莫西·兰德尔

我也在寻找无数小时来使UserManager.GetUserAsync工作,这是我找到缺少链接的唯一地方。谢谢!需要设置一个包含Claim的ClaimsIdentity,而不使用GenericIdentity。
Etienne Charland

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属性允许设置其值,这将是您的理想选择。

所以目标是到达那个物体。由于CoreHttpContext是抽象的,因此易于模拟。

假设控制器像

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.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();

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的原因。不能只从一个独立的阶级,像ProductionFactory获得用户
费利克斯·

1

我想直接打我的控制器,然后像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被填充,并且在我的Controller上调用方法时,我不需要做任何特殊的事情。

public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
    var requestedBy = User.Identity?.Name;
    ..................
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.