在 ASP.NET Core 中模拟 IPrincipal
Posted
技术标签:
【中文标题】在 ASP.NET Core 中模拟 IPrincipal【英文标题】:Mocking IPrincipal in ASP.NET Core 【发布时间】: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);
但这也引发了错误。 在文档中也找不到任何内容...
【问题讨论】:
【参考方案1】:控制器的User
is accessed 通过HttpContext
of the controller。后者is stored 内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
可以正常工作(如果您在代码中使用它来确定用户是否经过身份验证)。
【讨论】:
在我的例子中,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.IsAuthenticated
、Request.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")
);
【讨论】:
以上是关于在 ASP.NET Core 中模拟 IPrincipal的主要内容,如果未能解决你的问题,请参考以下文章
《ASP.NET Core 6框架揭秘》实例演示[27]:ASP.NET Core 6 Minimal API的模拟实现
ASP.NET Core 中的 Windows 身份验证模拟是不是已死?
如何保护 ASP.NET Core Web API 免受被盗 JWT 令牌以进行模拟
[ASP.NET Core Web应用上使用个人用户帐户存储在应用程序中的Postman模拟登录时出现400错误请求错误