// 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.Diagnostics; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting.Internal; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace Microsoft.AspNetCore.Hosting.Tests { public class HostingApplicationTests { [Fact] public void DisposeContextDoesNotThrowWhenContextScopeIsNull() { // Arrange var hostingApplication = CreateApplication(out var features); var context = hostingApplication.CreateContext(features); // Act/Assert hostingApplication.DisposeContext(context, null); } [Fact] public void ActivityIsNotCreatedWhenIsEnabledForActivityIsFalse() { var diagnosticSource = new DiagnosticListener("DummySource"); var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); bool eventsFired = false; bool isEnabledActivityFired = false; bool isEnabledStartFired = false; diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => { eventsFired |= pair.Key.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn"); }), (s, o, arg3) => { if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn") { Assert.IsAssignableFrom(o); isEnabledActivityFired = true; } if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") { isEnabledStartFired = true; } return false; }); hostingApplication.CreateContext(features); Assert.Null(Activity.Current); Assert.True(isEnabledActivityFired); Assert.False (isEnabledStartFired); Assert.False(eventsFired); } [Fact] public void ActivityIsCreatedButNotLoggedWhenIsEnabledForActivityStartIsFalse() { var diagnosticSource = new DiagnosticListener("DummySource"); var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); bool eventsFired = false; bool isEnabledStartFired = false; bool isEnabledActivityFired = false; diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => { eventsFired |= pair.Key.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn"); }), (s, o, arg3) => { if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn") { Assert.IsAssignableFrom(o); isEnabledActivityFired = true; return true; } if (s == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") { isEnabledStartFired = true; return false; } return true; }); hostingApplication.CreateContext(features); Assert.NotNull(Activity.Current); Assert.True(isEnabledActivityFired); Assert.True(isEnabledStartFired); Assert.False(eventsFired); } [Fact] public void ActivityIsCreatedAndLogged() { var diagnosticSource = new DiagnosticListener("DummySource"); var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); bool startCalled = false; diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => { if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start") { startCalled = true; Assert.NotNull(pair.Value); Assert.NotNull(Activity.Current); Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); AssertProperty(pair.Value, "HttpContext"); } })); hostingApplication.CreateContext(features); Assert.NotNull(Activity.Current); Assert.True(startCalled); } [Fact] public void ActivityIsStoppedDuringStopCall() { var diagnosticSource = new DiagnosticListener("DummySource"); var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); bool endCalled = false; diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => { if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") { endCalled = true; Assert.NotNull(Activity.Current); Assert.True(Activity.Current.Duration > TimeSpan.Zero); Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); AssertProperty(pair.Value, "HttpContext"); } })); var context = hostingApplication.CreateContext(features); hostingApplication.DisposeContext(context, null); Assert.True(endCalled); } [Fact] public void ActivityIsStoppedDuringUnhandledExceptionCall() { var diagnosticSource = new DiagnosticListener("DummySource"); var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); bool endCalled = false; diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => { if (pair.Key == "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") { endCalled = true; Assert.NotNull(Activity.Current); Assert.True(Activity.Current.Duration > TimeSpan.Zero); Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); AssertProperty(pair.Value, "HttpContext"); } })); var context = hostingApplication.CreateContext(features); hostingApplication.DisposeContext(context, new Exception()); Assert.True(endCalled); } [Fact] public void ActivityIsAvailableDuringUnhandledExceptionCall() { var diagnosticSource = new DiagnosticListener("DummySource"); var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); bool endCalled = false; diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => { if (pair.Key == "Microsoft.AspNetCore.Hosting.UnhandledException") { endCalled = true; Assert.NotNull(Activity.Current); Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); } })); var context = hostingApplication.CreateContext(features); hostingApplication.DisposeContext(context, new Exception()); Assert.True(endCalled); } [Fact] public void ActivityIsAvailibleDuringRequest() { var diagnosticSource = new DiagnosticListener("DummySource"); var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => { }), s => { if (s.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn")) { return true; } return false; }); hostingApplication.CreateContext(features); Assert.NotNull(Activity.Current); Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); } [Fact] public void ActivityParentIdAndBaggeReadFromHeaders() { var diagnosticSource = new DiagnosticListener("DummySource"); var hostingApplication = CreateApplication(out var features, diagnosticSource: diagnosticSource); diagnosticSource.Subscribe(new CallbackDiagnosticListener(pair => { }), s => { if (s.StartsWith("Microsoft.AspNetCore.Hosting.HttpRequestIn")) { return true; } return false; }); features.Set(new HttpRequestFeature() { Headers = new HeaderDictionary() { {"Request-Id", "ParentId1"}, {"Correlation-Context", "Key1=value1, Key2=value2"} } }); hostingApplication.CreateContext(features); Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", Activity.Current.OperationName); Assert.Equal("ParentId1", Activity.Current.ParentId); Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key1" && pair.Value == "value1"); Assert.Contains(Activity.Current.Baggage, pair => pair.Key == "Key2" && pair.Value == "value2"); } private static void AssertProperty(object o, string name) { Assert.NotNull(o); var property = o.GetType().GetTypeInfo().GetProperty(name, BindingFlags.Instance | BindingFlags.Public); Assert.NotNull(property); var value = property.GetValue(o); Assert.NotNull(value); Assert.IsAssignableFrom(value); } private static HostingApplication CreateApplication(out FeatureCollection features, DiagnosticListener diagnosticSource = null) { var httpContextFactory = new Mock(); features = new FeatureCollection(); features.Set(new HttpRequestFeature()); httpContextFactory.Setup(s => s.Create(It.IsAny())).Returns(new DefaultHttpContext(features)); httpContextFactory.Setup(s => s.Dispose(It.IsAny())); var hostingApplication = new HostingApplication( ctx => Task.FromResult(0), new NullScopeLogger(), diagnosticSource ?? new NoopDiagnosticSource(), httpContextFactory.Object); return hostingApplication; } private class NullScopeLogger : ILogger { public IDisposable BeginScope(TState state) => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { } } private class NoopDiagnosticSource : DiagnosticListener { public NoopDiagnosticSource() : base("DummyListener") { } public override bool IsEnabled(string name) => true; public override void Write(string name, object value) { } } private class CallbackDiagnosticListener : IObserver> { private readonly Action> _callback; public CallbackDiagnosticListener(Action> callback) { _callback = callback; } public void OnNext(KeyValuePair value) { _callback(value); } public void OnError(Exception error) { } public void OnCompleted() { } } } }