Request.GetOwinContext 在单元测试中返回 null - 如何在单元测试中测试 OWIN 身份验证?

Posted

技术标签:

【中文标题】Request.GetOwinContext 在单元测试中返回 null - 如何在单元测试中测试 OWIN 身份验证?【英文标题】:Request.GetOwinContext returns null within unit test - how do I test OWIN authentication within a unit test? 【发布时间】:2014-09-10 06:58:37 【问题描述】:

我目前正在尝试对我正在编写的使用 OWIN 进行身份验证的新 WebAPI 项目的身份验证进行单元测试,但在单元测试上下文中运行它时遇到问题。

这是我的测试方法:

[TestMethod]
public void TestRegister()

    using (WebApp.Start<Startup>("localhost/myAPI"))
    using (AccountController ac = new AccountController()
        
            Request = new System.Net.Http.HttpRequestMessage
                (HttpMethod.Post, "http://localhost/myAPI/api/Account/Register")
        )
    
        var result = ac.Register(new Models.RegisterBindingModel()
        
            Email = "testemail@testemail.com",
            Password = "Pass@word1",
            ConfirmPassword = "Pass@word1"
        ).Result;
        Assert.IsNotNull(result);
    

我在获得.Result 时收到AggregateException,但内部异常如下:

Result Message: 
Test method myAPI.Tests.Controllers.AccountControllerTest.TestRegister 
    threw exception: 
System.ArgumentNullException: Value cannot be null.
Parameter name: context
Result StackTrace:  
at Microsoft.AspNet.Identity.Owin.OwinContextExtensions
    .GetUserManager[TManager](IOwinContext context)
at myAPI.Controllers.AccountController.get_UserManager()
...

我已通过调试确认正在调用我的Startup 方法,调用ConfigurAuth

public void ConfigureAuth(IAppBuilder app)

    HttpConfiguration config = new HttpConfiguration();
    config.MapHttpAttributeRoutes();
    app.UseWebApi(config);

    // Configure the db context and user manager to use a single 
    //  instance per request
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>
        (ApplicationUserManager.Create);

    // Enable the application to use a cookie to store information for 
    //  the signed in user
    //  and to use a cookie to temporarily store information about a 
    //  user logging in with a third party login provider
    app.UseCookieAuthentication(new CookieAuthenticationOptions());
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

    // Configure the application for OAuth based flow
    PublicClientId = "self";
    OAuthOptions = new OAuthAuthorizationServerOptions
    
        TokenEndpointPath = new PathString("/Token"),
        Provider = new ApplicationOAuthProvider(PublicClientId),
        AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
        AllowInsecureHttp = true
    ;

    // Enable the application to use bearer tokens to authenticate users
    app.UseOAuthBearerTokens(OAuthOptions);

我尝试了一些方法,但似乎没有任何效果 - 我永远无法获得 OWIN 上下文。以下代码测试失败:

// POST api/Account/Register
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)

    if (!ModelState.IsValid)
    
        return BadRequest(ModelState);
    

    var user = new ApplicationUser() 
        UserName = model.Email, Email = model.Email ;

    IdentityResult result = await UserManager.CreateAsync(user, model.Password);

    if (!result.Succeeded)
    
        return GetErrorResult(result);
    

    return Ok();

这会调用UserManager 属性:

public ApplicationUserManager UserManager

    get
    
        return _userManager ?? Request.GetOwinContext()
           .GetUserManager<ApplicationUserManager>();
    
    private set
    
        _userManager = value;
    

它失败了:

return _userManager ?? Request.GetOwinContext()
    .GetUserManager<ApplicationUserManager>();

NullReferenceException - Request.GetOwinContext 正在返回 null

所以我的问题是:我是不是搞错了?我应该只测试 JSON 响应吗?或者有没有“内部”测试 OWIN 身份验证的好方法?

【问题讨论】:

为文字墙道歉 - 我想提供尽可能多的上下文。 :) 可惜没有一个答案是实际答案,而不是解决方法 确实如此。我不想嘲笑它,我想实际测试它。哦,好吧。 【参考方案1】:

这里的答案有所帮助,但并没有完全让我明白,这是一个完整的例子:

var userStore = new Mock<IUserStore<User>>();
var appUserMgrMock = new Mock<ApplicationUserManager>(userStore.Object);

var owin = new OwinContext();
owin.Set(appUserMgrMock.Object);

HttpContext.Current = new HttpContext(new HttpRequest(null, "http://test.com", null), new HttpResponse(null));
HttpContext.Current.Items["owin.Environment"] = owin.Environment;

记得安装所有必需的 nuget 包!

【讨论】:

【参考方案2】:
var data = new Dictionary<string, object>()

    "a", "b" // fake whatever  you need here.
;

ctx.Items["owin.Environment"] = data;

使用这段代码并添加到 HttpContext 而不是 ctx 和单元测试就像一个魅力。

【讨论】:

【参考方案3】:

GetOwinContext 调用 context.GetOwinEnvironment(); 这是

  private static IDictionary<string, object> GetOwinEnvironment(this HttpContextBase context)
    
        return (IDictionary<string, object>) context.Items[HttpContextItemKeys.OwinEnvironmentKey];
    

并且 HttpContextItemKeys.OwinEnvironmentKey 是一个常量“owin.Environment” 因此,如果您将其添加到 httpcontext 的项目中,它将起作用。

var request = new HttpRequest("", "http://google.com", "rUrl=http://www.google.com")
    
        ContentEncoding = Encoding.UTF8  //UrlDecode needs this to be set
    ;

    var ctx = new HttpContext(request, new HttpResponse(new StringWriter()));

    //Session need to be set
    var sessionContainer = new HttpSessionStateContainer("id", new SessionStateItemCollection(),
        new HttpStaticObjectsCollection(), 10, true,
        HttpCookieMode.AutoDetect,
        SessionStateMode.InProc, false);
    //this adds aspnet session
    ctx.Items["AspSession"] = typeof(HttpSessionState).GetConstructor(
        BindingFlags.NonPublic | BindingFlags.Instance,
        null, CallingConventions.Standard,
        new[]  typeof(HttpSessionStateContainer) ,
        null)
        .Invoke(new object[]  sessionContainer );

    var data = new Dictionary<string, object>()
    
        "a", "b" // fake whatever  you need here.
    ;

    ctx.Items["owin.Environment"] = data;

【讨论】:

【参考方案4】:

为确保在测试期间 OWIN 上下文可用(即,在调用 Request.GetOwinContext() 时修复空引用异常),您需要在测试项目中安装 Microsoft.AspNet.WebApi.Owin NuGet 包。安装后,您可以在请求中使用 SetOwinContext 扩展方法。

例子:

var controller = new MyController();
controller.Request = new HttpRequestMessage(HttpMethod.Post,
    new Uri("api/data/validate", UriKind.Relative)
    );
controller.Request.SetOwinContext(new OwinContext());

见https://msdn.microsoft.com/en-us/library/system.net.http.owinhttprequestmessageextensions.setowincontext%28v=vs.118%29.aspx

话虽如此,我同意您的特定用例的其他答案——在构造函数中提供一个 AppplicationUserManager 实例或工厂。如果您需要直接与您的测试将使用的上下文进行交互,则上述SetOwinContext 步骤是必要的。

【讨论】:

诀窍是添加 Microsoft.AspNet.WebApi.Owin。一旦我这样做了,那就很容易了。 controller.Request 在我的电脑上是只读的。是否有任何版本的 .net Mvc 不存在? 上述答案适用于 Web API 控制器(从 ApiController 继承的控制器。ASP.NET MVC 5.2.3 中的 MVC 控制器(从 Controller 继承)具有只读的 .Request 属性。进一步, ControllerBase.Request 在 ASP.NET Core 2.0.0 中是只读的。【参考方案5】:

我倾向于为 AccountController 注入一个用户管理器工厂。这样,您可以轻松交换测试中使用的用户管理器实例。您的默认工厂可以在构造函数中接受请求,以继续提供用户管理器的每个请求实例。你的测试工厂只是返回一个你想要提供测试的用户管理器的实例,我通常会选择一个 IUserStore 的存根实例,因此对用于存储身份信息的后端没有硬依赖。

工厂接口和类:

public interface IUserManagerFactory<TUser>
    where TUser : class, global::Microsoft.AspNet.Identity.IUser<string>

    UserManager<TUser> Create();



public class UserManagerFactory : IUserManagerFactory<AppUser>

    private HttpRequestMessage request;

    public UserManagerFactory(HttpRequestMessage request)
    
        if (request == null)
        
            throw new ArgumentNullException("request");
        

        this.request = request;
    

    public UserManager<AppUser, string> Create()
    
        return request.GetOwinContext().GetUserManager<UserManager<AppUser>>();
    

帐户控制器:

public AccountController(IUserManagerFactory<AppUser> userManagerFactory)

    this.userManagerFactory = userManagerFactory;


private UserManager<AppUser> userManager;

public UserManager<AppUser> UserManager

    get
    
        if (this.userManager == null)
        
            this.userManager = this.userManagerFactory.Create(); 
        

        return this.userManager;
    

测试工厂:

public class TestUserManagerFactory : IUserManagerFactory<AppUser>

    private IUserStore<AppUser> userStore;

    public TestUserManagerFactory()
    
        this.userStore = new MockUserStore();
    

    public UserManager<AppUser> Create()
     
        return new UserManager<AppUser>(new MockUserStore());
    

【讨论】:

【参考方案6】:

你可以只在AccountController的构造函数中传入UserManager,所以它不会试图在owinContext中找到它。默认构造函数对单元测试不友好。

【讨论】:

如何找到 UserManager?您如何在单元测试的上下文中找到它? 你可以新建一个(模拟或真实)进行单元测试

以上是关于Request.GetOwinContext 在单元测试中返回 null - 如何在单元测试中测试 OWIN 身份验证?的主要内容,如果未能解决你的问题,请参考以下文章

要引用这几个才有GetOwinContext与GetAutofacLifetimeScope

IsPersistent 在 OWIN Cookie 身份验证中的工作原理

如何快速在单例中使用此代码?

SmartClient:在单选项目之间插入控件

如何在单引号字符串中使用变量?

如何在单引号字符串中使用变量?