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

时间:2019-03-18 02:13:17

标签: c# asp.net-core azure-devops integration-testing

我有一个ASP.NET Core 2.2 Razor Pages Web App,我已经为跟踪the official guide编写了一些集成测试。

我可以使用dotnet test或Visual Studio内置的测试运行程序来使测试在本地运行。但是,在构建服务器(Azure DevOps Hosted 2017代理)上,测试将返回500错误。我认为它可能与Scott Hanselman's guide中所述的用户机密有关,但即使实施了他建议的某些修复程序(我认为我也不需要全部修复程序)后,我仍然遇到相同的错误:

  • builder.AddUserSecrets<Startup>();添加到了启动中。
  • 实施了CustomWebApplicationFactory,将环境设置为“开发”-下面的代码将此环境称为“生产”,以再现故障。

我也对this guide进行了理智检查,它更侧重于控制器,但是由于我在此阶段仅关心响应代码,因此可以达到我的目的。我已经下载了详细的日志,但对这些问题并没有任何了解。

我的代码如下:

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:

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);
            }
        }
    }
}

启动:

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();
        }
    }
}

错误输出:

[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 ---

修改

我不清楚我在哪里/为什么得到空引用,因为据我所知OpenIdConnectOptions配置是正确的(并且与AAD SSO兼容)。

2 个答案:

答案 0 :(得分:4)

事实证明,这是因为我没有在创建的.Build()对象上调用ConfigurationBuilder,而是将值分配给了{{1 }}类。这也意味着我将代码移至将机密包含到Configuration构造函数中。

即使如此,我的机密仍然无法在构建计算机上访问(这很有意义,因为它们是stored per machine)。因此,我还必须在构建管道中添加一个命令行任务-使用Startup命令添加测试所需的秘密。

答案 1 :(得分:0)

生产设置文件中是否可能缺少AzureAd配置部分?即appsettings.json与appsettings.Development.json