Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.TestHost" />
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
<Reference Include="Microsoft.Extensions.DependencyModel" />
<Reference Include="Microsoft.Extensions.Hosting" />
<Reference Include="Microsoft.Extensions.HostFactoryResolver.Sources" />
Expand Down
4 changes: 4 additions & 0 deletions src/Mvc/Mvc.Testing/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.Server.get -> Microsoft.AspNetCore.TestHost.TestServer!
Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.Initialize() -> void
Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.Server.get -> Microsoft.AspNetCore.TestHost.TestServer?
Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.UseKestrel() -> void
8 changes: 7 additions & 1 deletion src/Mvc/Mvc.Testing/src/Resources.resx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Expand Down Expand Up @@ -126,4 +126,10 @@
<data name="MissingDepsFile" xml:space="preserve">
<value>Can't find '{0}'. This file is required for functional tests to run properly. There should be a copy of the file on your source project bin folder. If that is not the case, make sure that the property PreserveCompilationContext is set to true on your project file. E.g '&lt;PreserveCompilationContext&gt;true&lt;/PreserveCompilationContext&gt;'. For functional tests to work they need to either run from the build output folder or the {1} file from your application's output directory must be copied to the folder where the tests are running on. A common cause for this error is having shadow copying enabled when the tests run.</value>
</data>
<data name="ServerNotInitialized" xml:space="preserve">
<value>Server hasn't been initialized yet. Plase intialized the server first before trying to create a client.</value>
</data>
<data name="UseKestrelCanBeCalledBeforeInitialization" xml:space="preserve">
<value>UseKestrel should be called before server initialization. Calling UseKestrel after the server was initialized will have no effect.</value>
</data>
</root>
141 changes: 123 additions & 18 deletions src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -24,11 +24,14 @@ namespace Microsoft.AspNetCore.Mvc.Testing;
/// Typically the Startup or Program classes can be used.</typeparam>
public partial class WebApplicationFactory<TEntryPoint> : IDisposable, IAsyncDisposable where TEntryPoint : class
{
private bool _useKestrel;
private bool _disposed;
private bool _disposedAsync;
private TestServer? _server;
private IHost? _host;
private Action<IWebHostBuilder> _configuration;
private IWebHost? _webHost;
private Uri? _webHostAddress;
private readonly List<HttpClient> _clients = new();
private readonly List<WebApplicationFactory<TEntryPoint>> _derivedFactories = new();

Expand Down Expand Up @@ -71,11 +74,11 @@ public WebApplicationFactory()
/// <summary>
/// Gets the <see cref="TestServer"/> created by this <see cref="WebApplicationFactory{TEntryPoint}"/>.
/// </summary>
public TestServer Server
public TestServer? Server
{
get
{
EnsureServer();
Initialize();
return _server;
}
}
Expand All @@ -87,8 +90,13 @@ public virtual IServiceProvider Services
{
get
{
EnsureServer();
return _host?.Services ?? _server.Host.Services;
Initialize();
if (_useKestrel)
{
return _webHost!.Services;
}

return _host?.Services ?? _server!.Host.Services;
}
}

Expand Down Expand Up @@ -136,10 +144,33 @@ internal virtual WebApplicationFactory<TEntryPoint> WithWebHostBuilderCore(Actio
return factory;
}

[MemberNotNull(nameof(_server))]
private void EnsureServer()
/// <summary>
/// Configures the factory to use Kestrel as the server.
/// </summary>
public void UseKestrel()
{
if (_server != null)
if (_server != null || _webHost != null)
{
throw new InvalidOperationException(Resources.UseKestrelCanBeCalledBeforeInitialization);
}

_useKestrel = true;
}

private static IWebHost CreateKestrelServer(IWebHostBuilder builder)
{
var host = builder.UseKestrel().Build();
host.Start();
return host;
}

/// <summary>
/// Initializes the instance by configurating the host builder.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the provided <typeparamref name="TEntryPoint"/> type has no factory method.</exception>
public void Initialize()
{
if (_server != null || _webHost != null)
{
return;
}
Expand Down Expand Up @@ -197,21 +228,45 @@ private void EnsureServer()
{
SetContentRoot(builder);
_configuration(builder);
_server = CreateServer(builder);
if (_useKestrel)
{
_webHost = CreateKestrelServer(builder);

var serverAddressFeature = _webHost.ServerFeatures.Get<IServerAddressesFeature>();
if (serverAddressFeature?.Addresses.Count > 0)
{
// Store the web host address as it's going to be used every time a client is created to communicate to the server
_webHostAddress = new Uri(serverAddressFeature.Addresses.Last());
ClientOptions.BaseAddress = _webHostAddress;
}
}
else
{
_server = CreateServer(builder);
}
}
}

[MemberNotNull(nameof(_server))]
private void ConfigureHostBuilder(IHostBuilder hostBuilder)
{
hostBuilder.ConfigureWebHost(webHostBuilder =>
{
SetContentRoot(webHostBuilder);
_configuration(webHostBuilder);
webHostBuilder.UseTestServer();
if (!_useKestrel)
{
webHostBuilder.UseTestServer();
}
else
{
webHostBuilder.UseKestrel();
}
});
_host = CreateHost(hostBuilder);
_server = (TestServer)_host.Services.GetRequiredService<IServer>();
if (!_useKestrel)
{
_server = (TestServer)_host.Services.GetRequiredService<IServer>();
}
}

private void SetContentRoot(IWebHostBuilder builder)
Expand Down Expand Up @@ -455,8 +510,19 @@ protected virtual void ConfigureWebHost(IWebHostBuilder builder)
/// redirects and handles cookies.
/// </summary>
/// <returns>The <see cref="HttpClient"/>.</returns>
public HttpClient CreateClient() =>
CreateClient(ClientOptions);
public HttpClient CreateClient()
{
var client = CreateClient(ClientOptions);

if (_useKestrel)
{
// Have to do this, as the ClientOptions.BaseAddress will be set to point to the kestrel server,
// and it may not match the original base address value.
client.BaseAddress = ClientOptions.BaseAddress;
}

return client;
}

/// <summary>
/// Creates an instance of <see cref="HttpClient"/> that automatically follows
Expand All @@ -476,12 +542,24 @@ public HttpClient CreateClient(WebApplicationFactoryClientOptions options) =>
/// <returns>The <see cref="HttpClient"/>.</returns>
public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers)
{
EnsureServer();
Initialize();

HttpClient client;
if (handlers == null || handlers.Length == 0)
{
client = _server.CreateClient();
if (_useKestrel)
{
client = new HttpClient();
}
else
{
if (_server is null)
{
throw new InvalidOperationException(Resources.ServerNotInitialized);
}

client = _server.CreateClient();
}
}
else
{
Expand All @@ -490,7 +568,7 @@ public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers)
handlers[i - 1].InnerHandler = handlers[i];
}

var serverHandler = _server.CreateHandler();
var serverHandler = CreateHandler();
handlers[^1].InnerHandler = serverHandler;

client = new HttpClient(handlers[0]);
Expand All @@ -503,6 +581,21 @@ public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers)
return client;
}

private HttpMessageHandler CreateHandler()
{
if (_useKestrel)
{
return new HttpClientHandler();
}

if (_server is null)
{
throw new InvalidOperationException(Resources.ServerNotInitialized);
}

return _server.CreateHandler();
}

/// <summary>
/// Configures <see cref="HttpClient"/> instances created by this <see cref="WebApplicationFactory{TEntryPoint}"/>.
/// </summary>
Expand All @@ -511,7 +604,19 @@ protected virtual void ConfigureClient(HttpClient client)
{
ArgumentNullException.ThrowIfNull(client);

client.BaseAddress = new Uri("http://localhost");
if (_useKestrel)
{
if (_webHost is null)
{
throw new InvalidOperationException(Resources.ServerNotInitialized);
}

client.BaseAddress = _webHostAddress;
}
else
{
client.BaseAddress = new Uri("http://localhost");
}
}

/// <summary>
Expand Down
14 changes: 14 additions & 0 deletions src/Mvc/test/Mvc.FunctionalTests/KestrelBasedWapFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Mvc.Testing;

namespace Microsoft.AspNetCore.Mvc.FunctionalTests;

public class KestrelBasedWapFactory : WebApplicationFactory<SimpleWebSite.Startup>
{
public KestrelBasedWapFactory() : base()
{
this.UseKestrel();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net;
using System.Net.Http.Headers;

namespace Microsoft.AspNetCore.Mvc.FunctionalTests;

public class RealServerBackedIntegrationTests : IClassFixture<KestrelBasedWapFactory>
{
public KestrelBasedWapFactory Factory { get; }

public RealServerBackedIntegrationTests(KestrelBasedWapFactory factory)
{
Factory = factory;
}

[Fact]
public async Task RetrievesDataFromRealServer()
{
// Arrange
var expectedMediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");

// Act
var client = Factory.CreateClient();
var response = await client.GetAsync("/");
var responseContent = await response.Content.ReadAsStringAsync();

// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedMediaType, response.Content.Headers.ContentType);

Assert.Equal(5000, client.BaseAddress.Port);

Assert.Contains("first", responseContent);
Assert.Contains("second", responseContent);
Assert.Contains("wall", responseContent);
Assert.Contains("floor", responseContent);
}
}
Loading