简体   繁体   English

ASP.NET核心集成测试在本地工作,但在生产环境中运行时会抛出空引用异常

[英]ASP.NET Core Integration Test works locally but throws null reference exception when running in production environment

I have an ASP.NET Core 2.2 Razor Pages Web App that I have written some integration tests for following the official guide . 我有一个ASP.NET Core 2.2 Razor Pages Web应用程序,我已经编写了一些集成测试,以遵循官方指南

I can get the tests to run locally using dotnet test or the test runners built into Visual Studio. 我可以使用dotnet test或Visual Studio中内置的测试运行程序在本地运行dotnet test However, on the build server (Azure DevOps Hosted 2017 agent) the tests will return a 500 error. 但是,在构建服务器(Azure DevOps Hosted 2017代理)上,测试将返回500错误。 I thought it might be related to user secrets as stated on Scott Hanselman's guide but I am still getting the same error, even after implementing some of his suggested fixes (I don't believe I need all of them) : 我认为它可能与Scott Hanselman指南中所述的用户机密有关但我仍然得到同样的错误,即使在实施了一些他建议的修复后(我不相信我需要所有这些):

  • Added builder.AddUserSecrets<Startup>(); 添加了builder.AddUserSecrets<Startup>(); to Startup. 启动。
  • Implemented a CustomWebApplicationFactory to set the environment to "Development" - code below has this as "Production" to reproduce the failure. 实现CustomWebApplicationFactory将环境设置为“开发” - 下面的代码将其作为“生产”来重现失败。

I also sanity checked against this guide which is more controller focused but since I only care about the response codes at this stage it serves my purpose. 我还对本指南进行了理智检查, 该指南更注重控制器,但由于我现在只关心响应代码,因此它符合我的目的。 I have downloaded the verbose logs and they don't shed any light on the issue. 我已经下载了详细的日志,他们没有说明问题。

My code is below: 我的代码如下:

CustomWebApplicationFactory: CustomWebApplicationFactory:

using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;

namespace WebPortal.Int.Tests
{
    /// <summary>
    /// Based on https://fullstackmark.com/post/20/painless-integration-testing-with-aspnet-core-web-api
    /// </summary>
    public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup> where TStartup : class
    {
        public CustomWebApplicationFactory() { }

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder
                .ConfigureTestServices(
                    services =>
                    {
                        services.Configure(AzureADDefaults.OpenIdScheme, (System.Action<OpenIdConnectOptions>)(o =>
                        {
                            // CookieContainer doesn't allow cookies from other paths
                            o.CorrelationCookie.Path = "/";
                            o.NonceCookie.Path = "/";
                        }));
                    }
                )
                .UseEnvironment("Production")
                .UseStartup<Startup>();

        }
    }
}

AuthenticationTests: AuthenticationTests:

using Microsoft.AspNetCore.Mvc.Testing;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace WebPortal.Int.Tests
{
    public class AuthenticationTests : IClassFixture<CustomWebApplicationFactory<Startup>>
    {
        private HttpClient _httpClient { get; }

        public AuthenticationTests(CustomWebApplicationFactory<Startup> fixture)
        {
            WebApplicationFactoryClientOptions webAppFactoryClientOptions = new WebApplicationFactoryClientOptions
            {
                // Disallow redirect so that we can check the following: Status code is redirect and redirect url is login url
                // As per https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.2#test-a-secure-endpoint
                AllowAutoRedirect = false
            };

            _httpClient = fixture.CreateClient(webAppFactoryClientOptions);
        }

        [Theory]
        [InlineData("/")]
        [InlineData("/Index")]
        [InlineData("/Error")]
        public async Task Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(string url)
        {
            // Act
            HttpResponseMessage response = await _httpClient.GetAsync(url);

            // Assert
            try
            {
                response.EnsureSuccessStatusCode();
            }
            catch (HttpRequestException ex)
            {
                Console.WriteLine(ex.Message, ex.InnerException.Message);
            }
        }
    }
}

Startup: 启动:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WebPortal.Authentication;
using WebPortal.Common.ConfigurationOptions;
using WebPortal.DataAccess;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;

namespace WebPortal
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => false;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddOptions<PowerBiSettings>()
                .Bind(Configuration.GetSection("PowerBI"))
                .ValidateDataAnnotations()
                .Validate(o => o.AreSettingsValid());

            services.AddOptions<AzureActiveDirectorySettings>()
                .Bind(Configuration.GetSection("AzureAd"))
                .ValidateDataAnnotations()
                .Validate(o => o.AreSettingsValid());

            services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
                .AddAzureAD(options => Configuration.Bind("AzureAd", options))
                .AddCookie();

            services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
            {
                options.Authority = options.Authority + "/v2.0/";
                options.TokenValidationParameters.ValidateIssuer = false;
            });

            services.AddTransient<Authentication.IAuthenticationHandler, AuthenticationHandler>();
            services.AddTransient<IReportRepository, ReportRepository>();

            services.AddHttpContextAccessor();

            services
                .AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
                .AddRazorPagesOptions(options =>
                {
                    options.Conventions.AuthorizePage("/Reports");
                });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();

                builder.AddUserSecrets<Startup>();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseAuthentication();

            app.UseMvc();
        }
    }
}

Error output: 错误输出:

[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.4.1 (64-bit .NET Core 4.6.27317.07)
[xUnit.net 00:00:01.30]   Discovering: WebPortal.Int.Tests
[xUnit.net 00:00:01.40]   Discovered:  WebPortal.Int.Tests
[xUnit.net 00:00:01.41]   Starting:    WebPortal.Int.Tests
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
    User profile is available. Using 'C:\Users\VssAdministrator\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[58]
    Creating key {dd820f09-8139-4d7d-954a-399923660f42} with creation date 2019-03-18 22:13:27Z, activation date 2019-03-18 22:13:27Z, and expiration date 2019-06-16 22:13:27Z.
info: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[39]
    Writing data to file 'C:\Users\VssAdministrator\AppData\Local\ASP.NET\DataProtection-Keys\key-dd820f09-8139-4d7d-954a-399923660f42.xml'.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
    Request starting HTTP/2.0 GET http://localhost/Index  
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
    Failed to determine the https port for redirect.
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
    An unhandled exception has occurred while executing the request.
System.ArgumentNullException: Value cannot be null.
Parameter name: uriString
at System.Uri..ctor(String uriString)
at Microsoft.AspNetCore.Authentication.AzureAD.UI.OpenIdConnectOptionsConfiguration.Configure(String name, OpenIdConnectOptions options)
at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
at Microsoft.Extensions.Options.OptionsMonitor`1.<>c__DisplayClass10_0.<Get>b__0()
at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
at System.Lazy`1.CreateValue()
at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd(String name, Func`1 createOptions)
at Microsoft.Extensions.Options.OptionsMonitor`1.Get(String name)
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[3]
    An exception was thrown attempting to execute the error handler.
System.ArgumentNullException: Value cannot be null.
Parameter name: uriString
at System.Uri..ctor(String uriString)
at Microsoft.AspNetCore.Authentication.AzureAD.UI.OpenIdConnectOptionsConfiguration.Configure(String name, OpenIdConnectOptions options)
at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
at Microsoft.Extensions.Options.OptionsMonitor`1.<>c__DisplayClass10_0.<Get>b__0()
at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
--- End of stack trace from previous location where exception was thrown ---
at System.Lazy`1.CreateValue()
at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd(String name, Func`1 createOptions)
at Microsoft.Extensions.Options.OptionsMonitor`1.Get(String name)
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
    Request finished in 449.9633ms 500 
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
    Request starting HTTP/2.0 GET http://localhost/Error  
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
    An unhandled exception has occurred while executing the request.
System.ArgumentNullException: Value cannot be null.
Parameter name: uriString
at System.Uri..ctor(String uriString)
at Microsoft.AspNetCore.Authentication.AzureAD.UI.OpenIdConnectOptionsConfiguration.Configure(String name, OpenIdConnectOptions options)
at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
at Microsoft.Extensions.Options.OptionsMonitor`1.<>c__DisplayClass10_0.<Get>b__0()
at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
--- End of stack trace from previous location where exception was thrown ---
at System.Lazy`1.CreateValue()
at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd(String name, Func`1 createOptions)
at Microsoft.Extensions.Options.OptionsMonitor`1.Get(String name)
at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
[xUnit.net 00:00:02.61]     WebPortal.Int.Tests.AuthenticationTests.Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(url: "/Index") [FAIL]
[xUnit.net 00:00:02.61]       System.ArgumentNullException : Value cannot be null.
[xUnit.net 00:00:02.61]       Parameter name: uriString
[xUnit.net 00:00:02.61]       Stack Trace:
[xUnit.net 00:00:02.61]            at System.Uri..ctor(String uriString)
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.Authentication.AzureAD.UI.OpenIdConnectOptionsConfiguration.Configure(String name, OpenIdConnectOptions options)
[xUnit.net 00:00:02.61]            at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name)
[xUnit.net 00:00:02.61]            at Microsoft.Extensions.Options.OptionsMonitor`1.<>c__DisplayClass10_0.<Get>b__0()
[xUnit.net 00:00:02.61]            at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
[xUnit.net 00:00:02.61]         --- End of stack trace from previous location where exception was thrown ---
[xUnit.net 00:00:02.61]            at System.Lazy`1.CreateValue()
[xUnit.net 00:00:02.61]            at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd(String name, Func`1 createOptions)
[xUnit.net 00:00:02.61]            at Microsoft.Extensions.Options.OptionsMonitor`1.Get(String name)
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.InitializeAsync(AuthenticationScheme scheme, HttpContext context)
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.Authentication.AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.TestHost.HttpContextBuilder.<>c__DisplayClass10_0.<<SendAsync>b__0>d.MoveNext()
[xUnit.net 00:00:02.61]         --- End of stack trace from previous location where exception was thrown ---
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.TestHost.ClientHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.Mvc.Testing.Handlers.CookieContainerHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
[xUnit.net 00:00:02.61]            at Microsoft.AspNetCore.Mvc.Testing.Handlers.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)[xUnit.net 00:00:02.63]     WebPortal.Int.Tests.AuthenticationTests.Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(url: "/Error") [FAIL]
[xUnit.net 00:00:02.64]     WebPortal.Int.Tests.AuthenticationTests.Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(url: "") [FAIL]

[xUnit.net 00:00:02.61]            at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
[xUnit.net 00:00:02.61]         D:\a\1\s\WebPortal.Int.Tests\AuthenticationTests.cs(24,0): at WebPortal.Int.Tests.AuthenticationTests.Get_PagesNotRequiringAuthenticationWithoutAuthentication_ReturnsSuccessCode(String url)
[xUnit.net 00:00:02.61]         --- End of stack trace from previous location where exception was thrown ---

edit 编辑

It isn't very clear to me where/why I am getting a null reference because as far as I can tell my OpenIdConnectOptions configuration is correct (and it works with AAD SSO). 我不清楚在哪里/为什么我得到一个空引用,因为据我所知,我的OpenIdConnectOptions配置是正确的(它适用于AAD SSO)。

It turns out that this was because I hadn't called .Build() on my ConfigurationBuilder object that I had created, AND assigned the value to the Configuration field inside the Startup class. 事实证明这是因为我没有在我创建的ConfigurationBuilder对象上调用.Build() ,并将值分配给Startup类中的Configuration字段。 This also meant that I moved the code to include the secrets into the Startup constructor. 这也意味着我移动了代码以将秘密包含在Startup构造函数中。

Even so, my secrets were still not accessible on the build machine (makes sense because they are stored per machine ). 即便如此,我的秘密仍然无法在构建机器上访问(因为它们是按机器存储的,所以有意义)。 So I had to add a Command Line task to my build pipeline as well - which uses the dotnet user-secrets set command to add the secrets in that are required for the tests. 所以我不得不在我的构建管道中添加一个命令行任务 - 它使用dotnet user-secrets set命令添加测试所需的秘密。

Is it possible that you were missing the AzureAd configuration section from you production settings file? 您是否可能缺少生产设置文件中的AzureAd配置部分? ie appsettings.json vs appsettings.Development.json 即appsettings.json vs appsettings.Development.json

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 执行单元测试时,asp.net 核心中的 TryValidateModel 抛出空引用异常 - TryValidateModel in asp.net core throws Null Reference Exception while performing unit test ASP.NET 核心单元测试抛出 Null 异常时测试 controller 问题响应 - ASP.NET Core unit test throws Null Exception when testing controller problem response ASP.NET Core(HttpSys)路由在本地工作,但在部署时不工作 - ASP.NET Core (HttpSys) Routing works locally but not when deployed 如何解决 asp.net 内核中 Httpcontext 中的 null 引用异常 - How to resolve null reference exception in Httpcontext in asp.net core 在 ASP.NET 核心 Razor 页面中填充下拉列表时获取 null 引用异常 - Getting null reference exception when populating dropdown list in ASP.NET Core Razor Pages asp.net core 数据库集成测试 - asp.net core Integration test for database 单击ASP.NET Webforms中的按钮时,空引用异常 - Null Reference Exception when Clicking a button in ASP.NET Webforms WebApplicationFactory 抛出 ASP.NET Core 集成测试中不存在 contentRootPath 的错误 - WebApplicationFactory throws error that contentRootPath does not exist in ASP.NET Core integration test ASP.NET Core 6 Web API 的集成测试抛出 System.InvalidOperationException - Integration test for ASP.NET Core 6 web API throws System.InvalidOperationException ASP.NET Core 2.0 Azure Webjobs SDK添加项目引用抛出异常 - ASP.NET Core 2.0 Azure Webjobs SDK adding project reference throws exception
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM