File: Internal\DefaultHttpResponseTests.cs
Web Access
Project: src\src\Http\Http\test\Microsoft.AspNetCore.Http.Tests.csproj (Microsoft.AspNetCore.Http.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.Globalization;
using Microsoft.AspNetCore.Http.Features;
using Moq;
 
namespace Microsoft.AspNetCore.Http;
 
public class DefaultHttpResponseTests
{
    [Theory]
    [InlineData(0)]
    [InlineData(9001)]
    [InlineData(65535)]
    public void GetContentLength_ReturnsParsedHeader(long value)
    {
        // Arrange
        var response = GetResponseWithContentLength(value.ToString(CultureInfo.InvariantCulture));
 
        // Act and Assert
        Assert.Equal(value, response.ContentLength);
    }
 
    [Fact]
    public void GetContentLength_ReturnsNullIfHeaderDoesNotExist()
    {
        // Arrange
        var response = GetResponseWithContentLength(contentLength: null);
 
        // Act and Assert
        Assert.Null(response.ContentLength);
    }
 
    [Theory]
    [InlineData("cant-parse-this")]
    [InlineData("-1000")]
    [InlineData("1000.00")]
    [InlineData("100/5")]
    public void GetContentLength_ReturnsNullIfHeaderCannotBeParsed(string contentLength)
    {
        // Arrange
        var response = GetResponseWithContentLength(contentLength);
 
        // Act and Assert
        Assert.Null(response.ContentLength);
    }
 
    [Fact]
    public void GetContentType_ReturnsNullIfHeaderDoesNotExist()
    {
        // Arrange
        var response = GetResponseWithContentType(contentType: null);
 
        // Act and Assert
        Assert.Null(response.ContentType);
    }
 
    [Fact]
    public void BodyWriter_CanGet()
    {
        var response = new DefaultHttpContext();
        var bodyPipe = response.Response.BodyWriter;
 
        Assert.NotNull(bodyPipe);
    }
 
    [Fact]
    public void ReplacingResponseBody_DoesNotCreateOnCompletedRegistration()
    {
        var features = new FeatureCollection();
 
        var originalStream = new FlushAsyncCheckStream();
        var replacementStream = new FlushAsyncCheckStream();
 
        var responseBodyMock = new Mock<IHttpResponseBodyFeature>();
        responseBodyMock.Setup(o => o.Stream).Returns(originalStream);
        features.Set(responseBodyMock.Object);
 
        var responseMock = new Mock<IHttpResponseFeature>();
        features.Set(responseMock.Object);
 
        var context = new DefaultHttpContext(features);
 
        Assert.Same(originalStream, context.Response.Body);
        Assert.Same(responseBodyMock.Object, context.Features.Get<IHttpResponseBodyFeature>());
 
        context.Response.Body = replacementStream;
 
        Assert.Same(replacementStream, context.Response.Body);
        Assert.NotSame(responseBodyMock.Object, context.Features.Get<IHttpResponseBodyFeature>());
 
        context.Response.Body = originalStream;
 
        Assert.Same(originalStream, context.Response.Body);
        Assert.Same(responseBodyMock.Object, context.Features.Get<IHttpResponseBodyFeature>());
 
        // The real issue was not that an OnCompleted registration existed, but that it would previously flush
        // the original response body in the OnCompleted callback after the response body was disposed.
        // However, since now there's no longer an OnCompleted registration at all, it's easier to verify that.
        // https://github.com/dotnet/aspnetcore/issues/25342
        responseMock.Verify(m => m.OnCompleted(It.IsAny<Func<object, Task>>(), It.IsAny<object>()), Times.Never);
    }
 
    [Fact]
    public async Task ResponseStart_CallsFeatureIfSet()
    {
        var features = new FeatureCollection();
        var mock = new Mock<IHttpResponseBodyFeature>();
        mock.Setup(o => o.StartAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
        features.Set(mock.Object);
 
        var responseMock = new Mock<IHttpResponseFeature>();
        responseMock.Setup(o => o.HasStarted).Returns(false);
        features.Set(responseMock.Object);
 
        var context = new DefaultHttpContext(features);
        await context.Response.StartAsync();
 
        mock.Verify(m => m.StartAsync(default), Times.Once());
    }
 
    [Fact]
    public async Task ResponseStart_CallsFeatureIfSetWithProvidedCancellationToken()
    {
        var features = new FeatureCollection();
 
        var mock = new Mock<IHttpResponseBodyFeature>();
        var ct = new CancellationToken();
        mock.Setup(o => o.StartAsync(It.Is<CancellationToken>((localCt) => localCt.Equals(ct)))).Returns(Task.CompletedTask);
        features.Set(mock.Object);
 
        var responseMock = new Mock<IHttpResponseFeature>();
        responseMock.Setup(o => o.HasStarted).Returns(false);
        features.Set(responseMock.Object);
 
        var context = new DefaultHttpContext(features);
        await context.Response.StartAsync(ct);
 
        mock.Verify(m => m.StartAsync(default), Times.Once());
    }
 
    [Fact]
    public async Task ResponseStart_DoesNotCallStartIfHasStartedIsTrue()
    {
        var features = new FeatureCollection();
 
        var startMock = new Mock<IHttpResponseBodyFeature>();
        startMock.Setup(o => o.StartAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
        features.Set(startMock.Object);
 
        var responseMock = new Mock<IHttpResponseFeature>();
        responseMock.Setup(o => o.HasStarted).Returns(true);
        features.Set(responseMock.Object);
 
        var context = new DefaultHttpContext(features);
        await context.Response.StartAsync();
 
        startMock.Verify(m => m.StartAsync(default), Times.Never());
    }
 
    [Fact]
    public async Task ResponseStart_CallsResponseBodyFlushIfNotSet()
    {
        var context = new DefaultHttpContext();
        var mock = new FlushAsyncCheckStream();
        context.Response.Body = mock;
 
        await context.Response.StartAsync(default);
 
        Assert.True(mock.IsCalled);
    }
 
    [Fact]
    public async Task RegisterForDisposeHandlesDisposeAsyncIfObjectImplementsIAsyncDisposable()
    {
        var features = new FeatureCollection();
        var response = new ResponseFeature();
        features.Set<IHttpResponseFeature>(response);
 
        var context = new DefaultHttpContext(features);
        var instance = new DisposableClass();
        context.Response.RegisterForDispose(instance);
 
        await response.ExecuteOnCompletedCallbacks();
 
        Assert.True(instance.DisposeAsyncCalled);
        Assert.False(instance.DisposeCalled);
    }
 
    public class ResponseFeature : IHttpResponseFeature
    {
        private readonly List<(Func<object, Task>, object)> _callbacks = new();
        public int StatusCode { get; set; }
        public string ReasonPhrase { get; set; }
        public IHeaderDictionary Headers { get; set; }
        public Stream Body { get; set; }
 
        public bool HasStarted => false;
 
        public void OnCompleted(Func<object, Task> callback, object state)
        {
            _callbacks.Add((callback, state));
        }
 
        public void OnStarting(Func<object, Task> callback, object state)
        {
            throw new NotImplementedException();
        }
 
        public async Task ExecuteOnCompletedCallbacks()
        {
            foreach (var (callback, state) in _callbacks)
            {
                await callback(state);
            }
        }
    }
 
    public class DisposableClass : IDisposable, IAsyncDisposable
    {
        public bool DisposeCalled { get; set; }
 
        public bool DisposeAsyncCalled { get; set; }
 
        public void Dispose()
        {
            DisposeCalled = true;
        }
 
        public ValueTask DisposeAsync()
        {
            DisposeAsyncCalled = true;
            return ValueTask.CompletedTask;
        }
    }
 
    private static HttpResponse CreateResponse(IHeaderDictionary headers)
    {
        var context = new DefaultHttpContext();
        context.Features.Get<IHttpResponseFeature>().Headers = headers;
        return context.Response;
    }
 
    private static HttpResponse GetResponseWithContentLength(string contentLength = null)
    {
        return GetResponseWithHeader("Content-Length", contentLength);
    }
 
    private static HttpResponse GetResponseWithContentType(string contentType = null)
    {
        return GetResponseWithHeader("Content-Type", contentType);
    }
 
    private static HttpResponse GetResponseWithHeader(string headerName, string headerValue)
    {
        var headers = new HeaderDictionary();
        if (headerValue != null)
        {
            headers.Add(headerName, headerValue);
        }
 
        return CreateResponse(headers);
    }
 
    private class FlushAsyncCheckStream : MemoryStream
    {
        public bool IsCalled { get; private set; }
 
        public override Task FlushAsync(CancellationToken cancellationToken)
        {
            IsCalled = true;
            return base.FlushAsync(cancellationToken);
        }
    }
}