File: Latency\RequestLatencyTelemetryMiddlewareTests.cs
Web Access
Project: src\test\Libraries\Microsoft.AspNetCore.Diagnostics.Middleware.Tests\Microsoft.AspNetCore.Diagnostics.Middleware.Tests.csproj (Microsoft.AspNetCore.Diagnostics.Middleware.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.AmbientMetadata;
using Microsoft.Extensions.Diagnostics.Latency;
using Microsoft.Extensions.Http.Diagnostics;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
 
namespace Microsoft.AspNetCore.Diagnostics.Latency.Test;
 
public class RequestLatencyTelemetryMiddlewareTests
{
    [Fact]
    public async Task RequestLatency_GivenContext_InvokesOperations()
    {
        var ex1 = new TestExporter();
        var ex2 = new TestExporter();
        string serverName = "AppServer";
        var nextInvoked = false;
        var m = new RequestLatencyTelemetryMiddleware((_) =>
        {
            nextInvoked = true;
            return Task.CompletedTask;
        },
        Options.Create(new RequestLatencyTelemetryOptions()), new List<ILatencyDataExporter> { ex1, ex2 },
        Options.Create(new ApplicationMetadata { ApplicationName = serverName }));
 
        var lc = GetMockLatencyContext();
        var httpContextMock = GetHttpContext(lc.Object);
        var fakeHttpResponseFeature = new FakeHttpResponseFeature();
        httpContextMock.Features.Set<IHttpResponseFeature>(fakeHttpResponseFeature);
 
        await m.InvokeAsync(httpContextMock);
        await fakeHttpResponseFeature.StartAsync();
        lc.Verify(c => c.Freeze());
        var header = httpContextMock.Response.Headers[TelemetryConstants.ServerApplicationNameHeader];
        Assert.True(header.Count > 0);
        Assert.Equal(serverName, header[0]);
        Assert.True(nextInvoked);
        Assert.True(ex1.Invoked == 1);
        Assert.True(ex2.Invoked == 1);
    }
 
    [Fact]
    public async Task RequestLatency_WithoutServiceMetadata_InvokesOperations()
    {
        var ex1 = new TestExporter();
        var ex2 = new TestExporter();
        var nextInvoked = false;
        var m = new RequestLatencyTelemetryMiddleware((_) =>
        {
            nextInvoked = true;
            return Task.CompletedTask;
        },
        Options.Create(new RequestLatencyTelemetryOptions()),
        new List<ILatencyDataExporter> { ex1, ex2 });
 
        var lc = GetMockLatencyContext();
        var httpContextMock = GetHttpContext(lc.Object);
        var fakeHttpResponseFeature = new FakeHttpResponseFeature();
        httpContextMock.Features.Set<IHttpResponseFeature>(fakeHttpResponseFeature);
 
        await m.InvokeAsync(httpContextMock);
        await fakeHttpResponseFeature.StartAsync();
        lc.Verify(c => c.Freeze());
        Assert.False(httpContextMock.Response.Headers.TryGetValue(TelemetryConstants.ServerApplicationNameHeader, out var val));
        Assert.True(nextInvoked);
        Assert.True(ex1.Invoked == 1);
        Assert.True(ex2.Invoked == 1);
    }
 
    [Fact]
    public async Task RequestLatency_WithServerNameHeadersSet_ReturnsLastServerName()
    {
        var ex1 = new TestExporter();
        var ex2 = new TestExporter();
        string serverName = "AppServer";
        var nextInvoked = false;
        var m = new RequestLatencyTelemetryMiddleware((_) =>
        {
            nextInvoked = true;
            return Task.CompletedTask;
        },
        Options.Create(new RequestLatencyTelemetryOptions()), new List<ILatencyDataExporter> { ex1, ex2 },
        Options.Create(new ApplicationMetadata { ApplicationName = serverName }));
        var lc = GetMockLatencyContext();
        var httpContextMock = GetHttpContext(lc.Object);
        var fakeHttpResponseFeature = new FakeHttpResponseFeature();
        httpContextMock.Features.Set<IHttpResponseFeature>(fakeHttpResponseFeature);
        httpContextMock.Response.Headers.Append(TelemetryConstants.ServerApplicationNameHeader, "testValue");
        await m.InvokeAsync(httpContextMock);
        await fakeHttpResponseFeature.StartAsync();
        lc.Verify(c => c.Freeze());
        var header = httpContextMock.Response.Headers[TelemetryConstants.ServerApplicationNameHeader];
        Assert.True(header.Count > 0);
        Assert.Equal(serverName, header[0]);
        Assert.True(nextInvoked);
        Assert.True(ex1.Invoked == 1);
        Assert.True(ex2.Invoked == 1);
    }
 
    [Fact]
    public async Task RequestLatency_NoServiceData_DoesNotAddHeader()
    {
        var ex1 = new TestExporter();
        var ex2 = new TestExporter();
        var nextInvoked = false;
        var m = new RequestLatencyTelemetryMiddleware((_) =>
        {
            nextInvoked = true;
            return Task.CompletedTask;
        },
        Options.Create(new RequestLatencyTelemetryOptions()),
        new List<ILatencyDataExporter> { ex1, ex2 },
        Options.Create(new ApplicationMetadata()));
 
        var lc = GetMockLatencyContext();
        var httpContextMock = GetHttpContext(lc.Object);
        var fakeHttpResponseFeature = new FakeHttpResponseFeature();
        httpContextMock.Features.Set<IHttpResponseFeature>(fakeHttpResponseFeature);
 
        await m.InvokeAsync(httpContextMock);
        await fakeHttpResponseFeature.StartAsync();
        lc.Verify(c => c.Freeze());
        Assert.False(httpContextMock.Response.Headers.TryGetValue(TelemetryConstants.ServerApplicationNameHeader, out var val));
        Assert.True(nextInvoked);
        Assert.True(ex1.Invoked == 1);
        Assert.True(ex2.Invoked == 1);
    }
 
    [Fact]
    public async Task RequestLatency_NoExporter()
    {
        var lc = GetMockLatencyContext();
        var httpContextMock = GetHttpContext(lc.Object);
        var nextInvoked = false;
        var m = new RequestLatencyTelemetryMiddleware((_) =>
        {
            nextInvoked = true;
            return Task.CompletedTask;
        },
        Options.Create(new RequestLatencyTelemetryOptions()),
        Array.Empty<ILatencyDataExporter>(),
        Options.Create(new ApplicationMetadata()));
 
        await m.InvokeAsync(httpContextMock);
 
        lc.Verify(c => c.Freeze());
        Assert.True(nextInvoked);
    }
 
    [Fact]
    public async Task RequestLatency_GivenTimeout_PassedToExport()
    {
        var exportTimeout = TimeSpan.FromSeconds(1);
        var ex1 = new TimeConsumingExporter(TimeSpan.FromSeconds(5));
 
        var nextInvoked = false;
        var m = new RequestLatencyTelemetryMiddleware((_) =>
            {
                nextInvoked = true;
                return Task.CompletedTask;
            },
            Options.Create(new RequestLatencyTelemetryOptions { LatencyDataExportTimeout = exportTimeout }),
            new List<ILatencyDataExporter> { ex1 },
            Options.Create(new ApplicationMetadata()));
 
        var lc = GetMockLatencyContext();
        var httpContextMock = GetHttpContext(lc.Object);
 
        await m.InvokeAsync(httpContextMock);
        await httpContextMock.Response.CompleteAsync();
 
        lc.Verify(c => c.Freeze());
        Assert.True(nextInvoked);
    }
 
    private sealed class FakeHttpResponseFeature : HttpResponseFeature
    {
        private Func<Task> _responseStartingAsync =
            static () => Task.CompletedTask;
 
        public override void OnStarting(Func<object, Task> callback, object state)
        {
            ChainCallback(callback, state);
        }
 
        public override void OnCompleted(Func<object, Task> callback, object state)
        {
            ChainCallback(callback, state);
        }
 
        private void ChainCallback(Func<object, Task> callback, object state)
        {
            var prior = _responseStartingAsync;
            _responseStartingAsync = async () =>
            {
                await prior();
                await callback(state);
            };
        }
 
        public async Task StartAsync() => await _responseStartingAsync();
    }
 
    private static HttpContext GetHttpContext(ILatencyContext latencyContext)
    {
        var httpContextMock = new DefaultHttpContext();
 
        var feature = new Mock<IHttpResponseFeature>();
        feature.Setup(m => m.OnCompleted(It.IsAny<Func<object, Task>>(), It.IsAny<object>()))
            .Callback<Func<object, Task>, object>((c, o) => c(o));
        httpContextMock.Features.Set(feature.Object);
 
        var serviceProviderMock = new Mock<IServiceProvider>();
        serviceProviderMock
            .Setup(serviceProvider => serviceProvider.GetService(typeof(ILatencyContext)))
            .Returns(latencyContext);
        httpContextMock.RequestServices = serviceProviderMock.Object;
 
        return httpContextMock;
    }
 
    private static Mock<ILatencyContext> GetMockLatencyContext()
    {
        var cc = new Mock<ILatencyContext>();
        return cc;
    }
 
    private class TestExporter : ILatencyDataExporter
    {
        public int Invoked { get; private set; }
 
        public async Task ExportAsync(LatencyData latencyData, CancellationToken cancellationToken)
        {
            Invoked++;
            await Task.CompletedTask;
        }
    }
 
    private class TimeConsumingExporter : ILatencyDataExporter
    {
        public int Invoked { get; private set; }
 
        private readonly TimeSpan _timeSpanToDelay;
 
        public TimeConsumingExporter(TimeSpan timeSpanToDelay)
        {
            _timeSpanToDelay = timeSpanToDelay;
        }
 
        public async Task ExportAsync(LatencyData latencyData, CancellationToken cancellationToken)
        {
            Invoked++;
 
            var e = await Record.ExceptionAsync(() => Task.Delay(_timeSpanToDelay, cancellationToken));
            Assert.IsAssignableFrom<OperationCanceledException>(e);
        }
    }
}