ChatGPT解决这个技术问题 Extra ChatGPT

在 ASP.NET Core 中模拟 IPrincipal

我有一个 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);
}

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


p
poke

控制器的 User is accessed 通过 HttpContext of the controllerControllerContext 中的后者 is stored

设置用户的最简单方法是为构造的用户分配不同的 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 正常工作(以防您在代码中使用它来确定用户是否经过身份验证)。


在我的例子中,它是 new Claim(ClaimTypes.Name, "1") 来匹配控制器对 user.Identity.Name 的使用;但除此之外,这正是我想要达到的目标...... Danke schon!
经过无数小时的搜索,这是最终让我摆脱困境的帖子。在我的核心 2.0 项目控制器方法中,我使用 User.FindFirstValue(ClaimTypes.NameIdentifier); 在我正在创建的对象上设置 userId,但由于主体为空而失败。这为我解决了这个问题。感谢您的精彩回答!
我还搜索了无数小时以使 UserManager.GetUserAsync 工作,这是我找到丢失链接的唯一地方。谢谢!需要设置一个包含声明的 ClaimsIdentity,而不是使用 GenericIdentity。
@JavierBufa 单一声明不支持多个值;相反,会有多个具有相同类型的声明。因此,传递给 ClaimsIdentity 构造函数的数组只能有多个角色声明,例如 new Claim(ClaimTypes.Role, "role1"), new Claim(ClaimTypes.Role, "role2")
@DeanP 这假设您处于创建控制器的单元测试中(此处:SomeController),并且您希望实例化该控制器,然后作为单元测试的一部分对其执行控制器操作。 – 您通常不希望在实际的应用程序代码中这样做。
N
Nkosi

在以前的版本中,您可以直接在控制器上设置 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 属性允许设置它的值,这将是您的方式。

所以目标是到达那个物体。在 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);
}

N
Nkosi

也可以使用现有的类,只在需要时模拟。

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

L
Luke

就我而言,我需要使用 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();

D
Devgig

我想直接点击我的控制器,然后像 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;
    ..................

J
James Wood

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

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

然后提供具体的类,一个提供 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 这样常见的东西,会有一些“开箱即用”的东西。但显然,不是!
另外,User 是 ControllerBase 的成员变量。这就是为什么在早期版本的 ASP.NET 中人们嘲笑 HttpContext,并从那里获取 IPrincipal。不能只从一个独立的类中获取用户,比如 ProductionFactory
K
Klepto

我有一个棕地 .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;
}

这使用 Moq,但可以适应其他模拟框架。身份验证类型被硬编码为“模拟”,因为我依赖默认身份验证,但这也可以提供。

它是这样使用的:

_controllerUnderTest.AddContextMock(
    claims: new[]
    {
        (ClaimTypes.Name, "UserName"),
        (ClaimTypes.MobilePhone, "1234"),
    },
    tokens: new[] 
    { 
        ("access_token", "accessTokenValue") 
    },
    headers: new[]
    {
        ("header", "headerValue")
    });