// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyModel; namespace Microsoft.AspNetCore.Mvc.Testing { /// /// Factory for bootstrapping an application in memory for functional end to end tests. /// /// A type in the entry point assembly of the application. /// Typically the Startup or Program classes can be used. public class WebApplicationFactory : IDisposable where TEntryPoint : class { private bool _disposed; private TestServer _server; private Action _configuration; private IList _clients = new List(); private List> _derivedFactories = new List>(); /// /// /// Creates an instance of . This factory can be used to /// create a instance using the MVC application defined by /// and one or more instances used to send to the . /// The will find the entry point class of /// assembly and initialize the application by calling IWebHostBuilder CreateWebHostBuilder(string [] args) /// on . /// /// /// This constructor will infer the application content root path by searching for a /// on the assembly containing the functional tests with /// a key equal to the assembly . /// In case an attribute with the right key can't be found, /// will fall back to searching for a solution file (*.sln) and then appending assembly name /// to the solution directory. The application root directory will be used to discover views and content files. /// /// /// The application assemblies will be loaded from the dependency context of the assembly containing /// . This means that project dependencies of the assembly containing /// will be loaded as application assemblies. /// /// public WebApplicationFactory() { _configuration = ConfigureWebHost; } /// /// Finalizes an instance of the class. /// ~WebApplicationFactory() { Dispose(false); } /// /// Gets the created by this . /// public TestServer Server => _server; /// /// Gets the of factories created from this factory /// by further customizing the when calling /// . /// public IReadOnlyList> Factories => _derivedFactories.AsReadOnly(); /// /// Gets the used by . /// public WebApplicationFactoryClientOptions ClientOptions { get; private set; } = new WebApplicationFactoryClientOptions(); /// /// Creates a new with a /// that is further customized by . /// /// /// An to configure the . /// /// A new . public WebApplicationFactory WithWebHostBuilder(Action configuration) => WithWebHostBuilderCore(configuration); internal virtual WebApplicationFactory WithWebHostBuilderCore(Action configuration) { var factory = new DelegatedWebApplicationFactory( ClientOptions, CreateServer, CreateWebHostBuilder, GetTestAssemblies, ConfigureClient, builder => { _configuration(builder); configuration(builder); }); _derivedFactories.Add(factory); return factory; } private void EnsureServer() { if (_server != null) { return; } EnsureDepsFile(); var builder = CreateWebHostBuilder(); SetContentRoot(builder); _configuration(builder); _server = CreateServer(builder); } private void SetContentRoot(IWebHostBuilder builder) { if (SetContentRootFromSetting(builder)) { return; } var metadataAttributes = GetContentRootMetadataAttributes( typeof(TEntryPoint).Assembly.FullName, typeof(TEntryPoint).Assembly.GetName().Name); string contentRoot = null; for (var i = 0; i < metadataAttributes.Length; i++) { var contentRootAttribute = metadataAttributes[i]; var contentRootCandidate = Path.Combine( AppContext.BaseDirectory, contentRootAttribute.ContentRootPath); var contentRootMarker = Path.Combine( contentRootCandidate, Path.GetFileName(contentRootAttribute.ContentRootTest)); if (File.Exists(contentRootMarker)) { contentRoot = contentRootCandidate; break; } } if (contentRoot != null) { builder.UseContentRoot(contentRoot); } else { builder.UseSolutionRelativeContentRoot(typeof(TEntryPoint).Assembly.GetName().Name); } } private static bool SetContentRootFromSetting(IWebHostBuilder builder) { // Attempt to look for TEST_CONTENTROOT_APPNAME in settings. This should result in looking for // ASPNETCORE_TEST_CONTENTROOT_APPNAME environment variable. var assemblyName = typeof(TEntryPoint).Assembly.GetName().Name; var settingSuffix = assemblyName.ToUpperInvariant().Replace(".", "_"); var settingName = $"TEST_CONTENTROOT_{settingSuffix}"; var settingValue = builder.GetSetting(settingName); if (settingValue == null) { return false; } builder.UseContentRoot(settingValue); return true; } private WebApplicationFactoryContentRootAttribute[] GetContentRootMetadataAttributes( string tEntryPointAssemblyFullName, string tEntryPointAssemblyName) { var testAssembly = GetTestAssemblies(); var metadataAttributes = testAssembly .SelectMany(a => a.GetCustomAttributes()) .Where(a => string.Equals(a.Key, tEntryPointAssemblyFullName, StringComparison.OrdinalIgnoreCase) || string.Equals(a.Key, tEntryPointAssemblyName, StringComparison.OrdinalIgnoreCase)) .OrderBy(a => a.Priority) .ToArray(); return metadataAttributes; } /// /// Gets the assemblies containing the functional tests. The /// applied to these /// assemblies defines the content root to use for the given /// . /// /// The list of containing tests. protected virtual IEnumerable GetTestAssemblies() { try { // The default dependency context will be populated in .net core applications. var context = DependencyContext.Default; if (context == null || context.CompileLibraries.Count == 0) { // The app domain friendly name will be populated in full framework. return new[] { Assembly.Load(AppDomain.CurrentDomain.FriendlyName) }; } var runtimeProjectLibraries = context.RuntimeLibraries .ToDictionary(r => r.Name, r => r, StringComparer.Ordinal); // Find the list of projects var projects = context.CompileLibraries.Where(l => l.Type == "project"); var entryPointAssemblyName = typeof(TEntryPoint).Assembly.GetName().Name; // Find the list of projects referencing TEntryPoint. var candidates = context.CompileLibraries .Where(library => library.Dependencies.Any(d => string.Equals(d.Name, entryPointAssemblyName, StringComparison.Ordinal))); var testAssemblies = new List(); foreach (var candidate in candidates) { if (runtimeProjectLibraries.TryGetValue(candidate.Name, out var runtimeLibrary)) { var runtimeAssemblies = runtimeLibrary.GetDefaultAssemblyNames(context); testAssemblies.AddRange(runtimeAssemblies.Select(Assembly.Load)); } } return testAssemblies; } catch (Exception) { } return Array.Empty(); } private void EnsureDepsFile() { if (typeof(TEntryPoint).Assembly.EntryPoint == null) { throw new InvalidOperationException(Resources.FormatInvalidAssemblyEntryPoint(typeof(TEntryPoint).Name)); } var depsFileName = $"{typeof(TEntryPoint).Assembly.GetName().Name}.deps.json"; var depsFile = new FileInfo(Path.Combine(AppContext.BaseDirectory, depsFileName)); if (!depsFile.Exists) { throw new InvalidOperationException(Resources.FormatMissingDepsFile( depsFile.FullName, Path.GetFileName(depsFile.FullName))); } } /// /// Creates a used to set up . /// /// /// The default implementation of this method looks for a public static IWebHostBuilder CreateDefaultBuilder(string[] args) /// method defined on the entry point of the assembly of and invokes it passing an empty string /// array as arguments. /// /// A instance. protected virtual IWebHostBuilder CreateWebHostBuilder() { var builder = WebHostBuilderFactory.CreateFromTypesAssemblyEntryPoint(Array.Empty()); if (builder == null) { throw new InvalidOperationException(Resources.FormatMissingCreateWebHostBuilderMethod( nameof(IWebHostBuilder), typeof(TEntryPoint).Assembly.EntryPoint.DeclaringType.FullName, typeof(WebApplicationFactory).Name, nameof(CreateWebHostBuilder))); } else { return builder.UseEnvironment("Development"); } } /// /// Creates the with the bootstrapped application in . /// /// The used to /// create the server. /// The with the bootstrapped application. protected virtual TestServer CreateServer(IWebHostBuilder builder) => new TestServer(builder); /// /// Gives a fixture an opportunity to configure the application before it gets built. /// /// The for the application. protected virtual void ConfigureWebHost(IWebHostBuilder builder) { } /// /// Creates an instance of that automatically follows /// redirects and handles cookies. /// /// The . public HttpClient CreateClient() => CreateClient(ClientOptions); /// /// Creates an instance of that automatically follows /// redirects and handles cookies. /// /// The . public HttpClient CreateClient(WebApplicationFactoryClientOptions options) => CreateDefaultClient(options.BaseAddress, options.CreateHandlers()); /// /// Creates a new instance of an that can be used to /// send to the server. The base address of the /// instance will be set to http://localhost. /// /// A list of instances to set up on the /// . /// The . public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers) { EnsureServer(); HttpClient client; if (handlers == null || handlers.Length == 0) { client = _server.CreateClient(); } else { for (var i = handlers.Length - 1; i > 0; i--) { handlers[i - 1].InnerHandler = handlers[i]; } var serverHandler = _server.CreateHandler(); handlers[handlers.Length - 1].InnerHandler = serverHandler; client = new HttpClient(handlers[0]); } _clients.Add(client); ConfigureClient(client); return client; } /// /// Configures instances created by this . /// /// The instance getting configured. protected virtual void ConfigureClient(HttpClient client) { if (client == null) { throw new ArgumentNullException(nameof(client)); } client.BaseAddress = new Uri("http://localhost"); } /// /// Creates a new instance of an that can be used to /// send to the server. /// /// The base address of the instance. /// A list of instances to set up on the /// . /// The . public HttpClient CreateDefaultClient(Uri baseAddress, params DelegatingHandler[] handlers) { var client = CreateDefaultClient(handlers); client.BaseAddress = baseAddress; return client; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// /// /// to release both managed and unmanaged resources; /// to release only unmanaged resources. /// protected virtual void Dispose(bool disposing) { if (_disposed) { return; } if (disposing) { foreach (var client in _clients) { client.Dispose(); } foreach (var factory in _derivedFactories) { factory.Dispose(); } _server?.Dispose(); } _disposed = true; } private class DelegatedWebApplicationFactory : WebApplicationFactory { private readonly Func _createServer; private readonly Func _createWebHostBuilder; private readonly Func> _getTestAssemblies; private readonly Action _configureClient; public DelegatedWebApplicationFactory( WebApplicationFactoryClientOptions options, Func createServer, Func createWebHostBuilder, Func> getTestAssemblies, Action configureClient, Action configureWebHost) { ClientOptions = new WebApplicationFactoryClientOptions(options); _createServer = createServer; _createWebHostBuilder = createWebHostBuilder; _getTestAssemblies = getTestAssemblies; _configureClient = configureClient; _configuration = configureWebHost; } protected override TestServer CreateServer(IWebHostBuilder builder) => _createServer(builder); protected override IWebHostBuilder CreateWebHostBuilder() => _createWebHostBuilder(); protected override IEnumerable GetTestAssemblies() => _getTestAssemblies(); protected override void ConfigureWebHost(IWebHostBuilder builder) => _configuration(builder); protected override void ConfigureClient(HttpClient client) => _configureClient(client); internal override WebApplicationFactory WithWebHostBuilderCore(Action configuration) { return new DelegatedWebApplicationFactory( ClientOptions, _createServer, _createWebHostBuilder, _getTestAssemblies, _configureClient, builder => { _configuration(builder); configuration(builder); }); } } } }