我有一个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兼容)。
答案 0 :(得分:4)
事实证明,这是因为我没有在创建的.Build()
对象上调用ConfigurationBuilder
,而是将值分配给了{{1 }}类。这也意味着我将代码移至将机密包含到Configuration
构造函数中。
即使如此,我的机密仍然无法在构建计算机上访问(这很有意义,因为它们是stored per machine)。因此,我还必须在构建管道中添加一个命令行任务-使用Startup
命令添加测试所需的秘密。
答案 1 :(得分:0)
生产设置文件中是否可能缺少AzureAd配置部分?即appsettings.json与appsettings.Development.json