【问题标题】:Mocking IPrincipal in ASP.NET Core在 ASP.NET Core 中模拟 IPrincipal
【发布时间】:2016-07-24 23:41:33
【问题描述】:

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

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

【问题讨论】:

    标签: c# unit-testing asp.net-core-mvc xunit.net


    【解决方案1】:

    控制器的User is accessed 通过HttpContext of the controller。后者is storedControllerContext

    设置用户的最简单方法是为构造的用户分配不同的 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。
    • @JavierBuffa 单一声明不支持多个值;相反,会有多个具有相同类型的声明。所以传递给 ClaimsIdentity 构造函数的数组只能有多个角色声明,例如new Claim(ClaimTypes.Role, "role1"), new Claim(ClaimTypes.Role, "role2").
    • @DeanP 这假设您在一个单元测试中,您正在创建一个控制器(这里:SomeController),并且您希望实例化该控制器,然后对其执行控制器操作作为你的单元测试。 – 您通常不希望在实际的应用程序代码中这样做。
    【解决方案2】:

    在以前的版本中,您可以直接在控制器上设置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);
    }
    

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

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

        【讨论】:

          【解决方案5】:

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

          【讨论】:

            【解决方案6】:

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

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

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

            【讨论】:

            • 谢谢。我正在为 my 对象做类似的事情。我只是希望像 IPrinicpal 这样常见的东西,会有一些“开箱即用”的东西。但显然不是!
            • 另外,User是ControllerBase的成员变量。这就是为什么在早期版本的 ASP.NET 中人们嘲笑 HttpContext,并从那里获取 IPrincipal。不能只从一个独立的类中获取 User,比如 ProductionFactory
            【解决方案7】:

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

            【讨论】:

              猜你喜欢
              • 2023-04-08
              • 1970-01-01
              • 1970-01-01
              • 2016-05-12
              • 1970-01-01
              • 2019-02-10
              • 2020-06-19
              相关资源
              最近更新 更多