// 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; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting.Fakes; using Microsoft.AspNet.Hosting.Server; using Microsoft.AspNet.Hosting.Startup; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Server.Features; using Microsoft.AspNet.Testing.xunit; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.AspNet.Hosting { public class WebHostTests : IServerFactory, IServer { private readonly IList _startInstances = new List(); private IFeatureCollection _featuresSupportedByThisHost = NewFeatureCollection(); private IFeatureCollection _instanceFeaturesSupportedByThisHost; public IFeatureCollection Features { get { var features = new FeatureCollection(); foreach (var feature in _featuresSupportedByThisHost) { features[feature.Key] = feature.Value; } if (_instanceFeaturesSupportedByThisHost != null) { foreach (var feature in _instanceFeaturesSupportedByThisHost) { features[feature.Key] = feature.Value; } } return features; } } static IFeatureCollection NewFeatureCollection() { var stub = new StubFeatures(); var features = new FeatureCollection(); features[typeof(IHttpRequestFeature)] = stub; features[typeof(IHttpResponseFeature)] = stub; return features; } [Fact] public void WebHostThrowsWithNoServer() { var ex = Assert.Throws(() => CreateBuilder().Build().Start()); Assert.True(ex.Message.Contains("UseServer()")); } [Fact] public void UseStartupThrowsWithNull() { Assert.Throws(() => CreateBuilder().UseStartup((string)null)); } [Fact] public void CanStartWithOldServerConfig() { var vals = new Dictionary { { "server", "Microsoft.AspNet.Hosting.Tests" } }; var builder = new ConfigurationBuilder() .AddInMemoryCollection(vals); var config = builder.Build(); var host = CreateBuilder(config).Build(); host.Start(); Assert.NotNull(host.Services.GetService()); } [Fact] public void CanStartWithServerConfig() { var vals = new Dictionary { { "Server", "Microsoft.AspNet.Hosting.Tests" } }; var builder = new ConfigurationBuilder() .AddInMemoryCollection(vals); var config = builder.Build(); var host = CreateBuilder(config).Build(); host.Start(); Assert.NotNull(host.Services.GetService()); } [Fact] public void CanDefaultAddresseIfNotConfigured() { var vals = new Dictionary { { "Server", "Microsoft.AspNet.Hosting.Tests" } }; var builder = new ConfigurationBuilder() .AddInMemoryCollection(vals); var config = builder.Build(); var host = CreateBuilder(config).Build(); host.Start(); Assert.NotNull(host.Services.GetService()); Assert.Equal("http://localhost:5000", host.ServerFeatures.Get().Addresses.First()); } [Fact] public void FlowsConfig() { var vals = new Dictionary { { "Server", "Microsoft.AspNet.Hosting.Tests" } }; var builder = new ConfigurationBuilder() .AddInMemoryCollection(vals); var config = builder.Build(); var host = CreateBuilder(config).Build(); host.Start(); var hostingEnvironment = host.Services.GetService(); Assert.NotNull(hostingEnvironment.Configuration); Assert.Equal("Microsoft.AspNet.Hosting.Tests", hostingEnvironment.Configuration["Server"]); } [Fact] public void WebHostCanBeStarted() { var host = CreateBuilder() .UseServer((IServerFactory)this) .UseStartup("Microsoft.AspNet.Hosting.Tests") .Start(); Assert.NotNull(host); Assert.Equal(1, _startInstances.Count); Assert.Equal(0, _startInstances[0].DisposeCalls); host.Dispose(); Assert.Equal(1, _startInstances[0].DisposeCalls); } [Fact] public void WebHostShutsDownWhenTokenTriggers() { var host = CreateBuilder() .UseServer((IServerFactory)this) .UseStartup("Microsoft.AspNet.Hosting.Tests") .Build(); var lifetime = host.Services.GetRequiredService(); var cts = new CancellationTokenSource(); Task.Run(() => host.Run(cts.Token)); // Wait on the host to be started lifetime.ApplicationStarted.WaitHandle.WaitOne(); Assert.Equal(1, _startInstances.Count); Assert.Equal(0, _startInstances[0].DisposeCalls); cts.Cancel(); // Wait on the host to shutdown lifetime.ApplicationStopped.WaitHandle.WaitOne(); Assert.Equal(1, _startInstances[0].DisposeCalls); } [Fact] public void WebHostDisposesServiceProvider() { var host = CreateBuilder() .UseServer((IServerFactory)this) .ConfigureServices(s => { s.AddTransient(); s.AddSingleton(); }) .UseStartup("Microsoft.AspNet.Hosting.Tests") .Build(); host.Start(); var singleton = (FakeService)host.Services.GetService(); var transient = (FakeService)host.Services.GetService(); Assert.False(singleton.Disposed); Assert.False(transient.Disposed); host.Dispose(); Assert.True(singleton.Disposed); Assert.True(transient.Disposed); } [Fact] public void WebHostNotifiesApplicationStarted() { var host = CreateBuilder() .UseServer((IServerFactory)this) .Build(); var applicationLifetime = host.Services.GetService(); Assert.False(applicationLifetime.ApplicationStarted.IsCancellationRequested); using (host) { host.Start(); Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested); } } [Fact] public void WebHostInjectsHostingEnvironment() { var host = CreateBuilder() .UseServer((IServerFactory)this) .UseStartup("Microsoft.AspNet.Hosting.Tests") .UseEnvironment("WithHostingEnvironment") .Build(); using (host) { host.Start(); var env = host.Services.GetService(); Assert.Equal("Changed", env.EnvironmentName); } } [Fact] public void CanReplaceStartupLoader() { var builder = CreateBuilder() .ConfigureServices(services => { services.AddTransient(); }) .UseServer((IServerFactory)this) .UseStartup("Microsoft.AspNet.Hosting.Tests"); Assert.Throws(() => builder.Build()); } [Fact] public void CanCreateApplicationServicesWithAddedServices() { var host = CreateBuilder().UseServer((IServerFactory)this).ConfigureServices(services => services.AddOptions()).Build(); Assert.NotNull(host.Services.GetRequiredService>()); } [Fact] public void EnvDefaultsToProductionIfNoConfig() { var host = CreateBuilder().UseServer((IServerFactory)this).Build(); var env = host.Services.GetService(); Assert.Equal(EnvironmentName.Production, env.EnvironmentName); } [Fact] public void EnvDefaultsToConfigValueIfSpecifiedWithOldKey() { var vals = new Dictionary { // Old key is actualy ASPNET_ENV but WebHostConfiguration expects environment // variable names stripped from ASPNET_ prefix so using just ENV here { "ENV", "Staging" } }; var builder = new ConfigurationBuilder() .AddInMemoryCollection(vals); var config = builder.Build(); var host = CreateBuilder(config).UseServer((IServerFactory)this).Build(); var env = host.Services.GetService(); Assert.Equal("Staging", env.EnvironmentName); } [Fact] public void EnvDefaultsToConfigValueIfSpecified() { var vals = new Dictionary { { "Environment", "Staging" } }; var builder = new ConfigurationBuilder() .AddInMemoryCollection(vals); var config = builder.Build(); var host = CreateBuilder(config).UseServer((IServerFactory)this).Build(); var env = host.Services.GetService(); Assert.Equal("Staging", env.EnvironmentName); } [Fact(Skip = "Missing content publish property")] public void WebRootCanBeResolvedFromTheConfig() { var vals = new Dictionary { { "webroot", "testroot" } }; var builder = new ConfigurationBuilder() .AddInMemoryCollection(vals); var config = builder.Build(); var host = CreateBuilder(config).UseServer((IServerFactory)this).Build(); var env = host.Services.GetService(); Assert.Equal(Path.GetFullPath("testroot"), env.WebRootPath); Assert.True(env.WebRootFileProvider.GetFileInfo("TextFile.txt").Exists); } [Fact] public void IsEnvironment_Extension_Is_Case_Insensitive() { var host = CreateBuilder().UseServer((IServerFactory)this).Build(); using (host) { host.Start(); var env = host.Services.GetRequiredService(); Assert.True(env.IsEnvironment(EnvironmentName.Production)); Assert.True(env.IsEnvironment("producTion")); } } [Theory] [InlineData(null, "")] [InlineData("", "")] [InlineData("/", "/")] [InlineData(@"\", @"\")] [InlineData("sub", "sub")] [InlineData("sub/sub2/sub3", @"sub/sub2/sub3")] public void MapPath_Facts(string virtualPath, string expectedSuffix) { RunMapPath(virtualPath, expectedSuffix); } [ConditionalTheory] [OSSkipCondition(OperatingSystems.Linux)] [OSSkipCondition(OperatingSystems.MacOSX)] [InlineData(@"sub/sub2\sub3\", @"sub/sub2/sub3/")] public void MapPath_Windows_Facts(string virtualPath, string expectedSuffix) { RunMapPath(virtualPath, expectedSuffix); } [Fact] public void WebHost_CreatesDefaultRequestIdentifierFeature_IfNotPresent() { // Arrange HttpContext httpContext = null; var requestDelegate = new RequestDelegate(innerHttpContext => { httpContext = innerHttpContext; return Task.FromResult(0); }); var host = CreateHost(requestDelegate); // Act host.Start(); // Assert Assert.NotNull(httpContext); var featuresTraceIdentifier = httpContext.Features.Get().TraceIdentifier; Assert.False(string.IsNullOrWhiteSpace(httpContext.TraceIdentifier)); Assert.Same(httpContext.TraceIdentifier, featuresTraceIdentifier); } [Fact] public void WebHost_DoesNot_CreateDefaultRequestIdentifierFeature_IfPresent() { // Arrange HttpContext httpContext = null; var requestDelegate = new RequestDelegate(innerHttpContext => { httpContext = innerHttpContext; return Task.FromResult(0); }); var requestIdentifierFeature = new StubHttpRequestIdentifierFeature(); _featuresSupportedByThisHost[typeof(IHttpRequestIdentifierFeature)] = requestIdentifierFeature; var host = CreateHost(requestDelegate); // Act host.Start(); // Assert Assert.NotNull(httpContext); Assert.Same(requestIdentifierFeature, httpContext.Features.Get()); } [Fact] public void WebHost_InvokesConfigureMethodsOnlyOnce() { var host = CreateBuilder() .UseServer((IServerFactory)this) .UseStartup() .Build(); using (host) { host.Start(); var services = host.Services; var services2 = host.Services; Assert.Equal(1, CountStartup.ConfigureCount); Assert.Equal(1, CountStartup.ConfigureServicesCount); } } public class CountStartup { public static int ConfigureServicesCount; public static int ConfigureCount; public void ConfigureServices(IServiceCollection services) { ConfigureServicesCount++; } public void Configure(IApplicationBuilder app) { ConfigureCount++; } } [Fact] public void WebHost_ThrowsForBadConfigureServiceSignature() { var builder = CreateBuilder() .UseServer((IServerFactory)this) .UseStartup(); var ex = Assert.Throws(() => builder.Build()); Assert.True(ex.Message.Contains("ConfigureServices")); } public class BadConfigureServicesStartup { public void ConfigureServices(IServiceCollection services, int gunk) { } public void Configure(IApplicationBuilder app) { } } private IWebHost CreateHost(RequestDelegate requestDelegate) { var builder = CreateBuilder() .UseServer((IServerFactory)this) .Configure( appBuilder => { appBuilder.ApplicationServices.GetRequiredService().AddProvider(new AllMessagesAreNeeded()); appBuilder.Run(requestDelegate); }); return builder.Build(); } private void RunMapPath(string virtualPath, string expectedSuffix) { var host = CreateBuilder().UseServer((IServerFactory)this).Build(); using (host) { host.Start(); var env = host.Services.GetRequiredService(); // MapPath requires webroot to be set, we don't care // about file provider so just set it here env.WebRootPath = "."; var mappedPath = env.MapPath(virtualPath); expectedSuffix = expectedSuffix.Replace('/', Path.DirectorySeparatorChar); Assert.Equal(Path.Combine(env.WebRootPath, expectedSuffix), mappedPath); } } private IWebHostBuilder CreateBuilder(IConfiguration config = null) { return new WebHostBuilder().UseConfiguration(config ?? new ConfigurationBuilder().Build()).UseStartup("Microsoft.AspNet.Hosting.Tests"); } public void Start(IHttpApplication application) { var startInstance = new StartInstance(); _startInstances.Add(startInstance); var context = application.CreateContext(Features); try { application.ProcessRequestAsync(context); } catch (Exception ex) { application.DisposeContext(context, ex); throw; } application.DisposeContext(context, null); } public void Dispose() { if (_startInstances != null) { foreach (var startInstance in _startInstances) { startInstance.Dispose(); } } } public IServer CreateServer(IConfiguration configuration) { _instanceFeaturesSupportedByThisHost = new FeatureCollection(); _instanceFeaturesSupportedByThisHost.Set(new ServerAddressesFeature()); return this; } private class StartInstance : IDisposable { public int DisposeCalls { get; set; } public void Dispose() { DisposeCalls += 1; } } private class TestLoader : IStartupLoader { public Type FindStartupType(string startupAssemblyName, IList diagnosticMessages) { throw new NotImplementedException(); } public StartupMethods LoadMethods(Type startupType, IList diagnosticMessages) { throw new NotImplementedException(); } } private class ReadOnlyFeatureCollection : IFeatureCollection { public object this[Type key] { get { return null; } set { throw new NotSupportedException(); } } public bool IsReadOnly { get { return true; } } public int Revision { get { return 0; } } public void Dispose() { } public TFeature Get() { return default(TFeature); } public IEnumerator> GetEnumerator() { yield break; } public void Set(TFeature instance) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() { yield break; } } private class ServerAddressesFeature : IServerAddressesFeature { public ICollection Addresses { get; } = new List(); } private class AllMessagesAreNeeded : ILoggerProvider, ILogger { public bool IsEnabled(LogLevel logLevel) => true; public ILogger CreateLogger(string name) => this; public IDisposable BeginScopeImpl(object state) { var stringified = state.ToString(); return this; } public void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func formatter) { var stringified = formatter(state, exception); } public void Dispose() { } } private class StubFeatures : IHttpRequestFeature, IHttpResponseFeature, IHeaderDictionary { public StubFeatures() { Headers = this; Body = new MemoryStream(); } public StringValues this[string key] { get { return StringValues.Empty; } set { } } public Stream Body { get; set; } public int Count => 0; public bool HasStarted { get; set; } public IHeaderDictionary Headers { get; set; } public bool IsReadOnly => false; public ICollection Keys => null; public string Method { get; set; } public string Path { get; set; } public string PathBase { get; set; } public string Protocol { get; set; } public string QueryString { get; set; } public string ReasonPhrase { get; set; } public string Scheme { get; set; } public int StatusCode { get; set; } public ICollection Values => null; public void Add(KeyValuePair item) { } public void Add(string key, StringValues value) { } public void Clear() { } public bool Contains(KeyValuePair item) => false; public bool ContainsKey(string key) => false; public void CopyTo(KeyValuePair[] array, int arrayIndex) { } public IEnumerator> GetEnumerator() => null; public void OnCompleted(Func callback, object state) { } public void OnStarting(Func callback, object state) { } public bool Remove(KeyValuePair item) => false; public bool Remove(string key) => false; public bool TryGetValue(string key, out StringValues value) { value = StringValues.Empty; return false; } IEnumerator IEnumerable.GetEnumerator() => null; } private class StubHttpRequestIdentifierFeature : IHttpRequestIdentifierFeature { public string TraceIdentifier { get; set; } } } }