|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System.Buffers;
using System.IO.Pipelines;
using System.Net.Http;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Moq;
namespace Microsoft.AspNetCore.OutputCaching.Tests;
internal class TestUtils
{
static TestUtils()
{
// Force sharding in tests
StreamUtilities.BodySegmentSize = 10;
}
private static bool TestRequestDelegate(HttpContext context, string guid)
{
var headers = context.Response.GetTypedHeaders();
headers.Date = DateTimeOffset.UtcNow;
headers.Headers["X-Value"] = guid;
if (context.Request.Method != "HEAD")
{
return true;
}
return false;
}
internal static async Task TestRequestDelegateWriteAsync(HttpContext context)
{
var uniqueId = Guid.NewGuid().ToString();
if (TestRequestDelegate(context, uniqueId))
{
await context.Response.WriteAsync(uniqueId);
}
}
internal static async Task TestRequestDelegateSendFileAsync(HttpContext context)
{
var path = Path.Combine(AppContext.BaseDirectory, "TestDocument.txt");
var uniqueId = Guid.NewGuid().ToString();
if (TestRequestDelegate(context, uniqueId))
{
await context.Response.SendFileAsync(path, 0, null);
await context.Response.WriteAsync(uniqueId);
}
}
internal static Task TestRequestDelegateWrite(HttpContext context)
{
var uniqueId = Guid.NewGuid().ToString();
if (TestRequestDelegate(context, uniqueId))
{
var feature = context.Features.Get<IHttpBodyControlFeature>();
if (feature != null)
{
feature.AllowSynchronousIO = true;
}
context.Response.Write(uniqueId);
}
return Task.CompletedTask;
}
internal static async Task TestRequestDelegatePipeWriteAsync(HttpContext context)
{
var uniqueId = Guid.NewGuid().ToString();
if (TestRequestDelegate(context, uniqueId))
{
Encoding.UTF8.GetBytes(uniqueId, context.Response.BodyWriter);
await context.Response.BodyWriter.FlushAsync();
}
}
internal static IOutputCacheKeyProvider CreateTestKeyProvider()
{
return CreateTestKeyProvider(new OutputCacheOptions());
}
internal static IOutputCacheKeyProvider CreateTestKeyProvider(OutputCacheOptions options)
{
return new OutputCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
}
internal static IEnumerable<IHostBuilder> CreateBuildersWithOutputCaching(
Action<IApplicationBuilder>? configureDelegate = null,
OutputCacheOptions? options = null,
Action<HttpContext>? contextAction = null)
{
return CreateBuildersWithOutputCaching(configureDelegate, options, new RequestDelegate[]
{
context =>
{
contextAction?.Invoke(context);
return TestRequestDelegateWrite(context);
},
context =>
{
contextAction?.Invoke(context);
return TestRequestDelegateWriteAsync(context);
},
context =>
{
contextAction?.Invoke(context);
return TestRequestDelegateSendFileAsync(context);
},
context =>
{
contextAction?.Invoke(context);
return TestRequestDelegatePipeWriteAsync(context);
},
});
}
private static IEnumerable<IHostBuilder> CreateBuildersWithOutputCaching(
Action<IApplicationBuilder>? configureDelegate = null,
OutputCacheOptions? options = null,
IEnumerable<RequestDelegate>? requestDelegates = null)
{
if (configureDelegate == null)
{
configureDelegate = app => { };
}
if (requestDelegates == null)
{
requestDelegates = new RequestDelegate[]
{
TestRequestDelegateWriteAsync,
TestRequestDelegateWrite,
TestRequestDelegatePipeWriteAsync,
};
}
foreach (var requestDelegate in requestDelegates)
{
// Test with in memory OutputCache
yield return new HostBuilder()
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddOutputCache(outputCachingOptions =>
{
Assert.NotNull(outputCachingOptions.ApplicationServices);
if (options != null)
{
outputCachingOptions.MaximumBodySize = options.MaximumBodySize;
outputCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths;
outputCachingOptions.TimeProvider = options.TimeProvider;
outputCachingOptions.BasePolicies = options.BasePolicies;
outputCachingOptions.DefaultExpirationTimeSpan = options.DefaultExpirationTimeSpan;
outputCachingOptions.SizeLimit = options.SizeLimit;
}
else
{
outputCachingOptions.BasePolicies = new();
outputCachingOptions.BasePolicies.Add(new OutputCachePolicyBuilder().Build());
}
});
})
.Configure(app =>
{
configureDelegate(app);
app.UseOutputCache();
app.Run(requestDelegate);
});
});
}
}
internal static OutputCacheMiddleware CreateTestMiddleware(
RequestDelegate? next = null,
IOutputCacheStore? cache = null,
OutputCacheOptions? options = null,
TestSink? testSink = null,
IOutputCacheKeyProvider? keyProvider = null
)
{
if (next == null)
{
next = httpContext => Task.CompletedTask;
}
if (cache == null)
{
cache = new SimpleTestOutputCache();
}
if (options == null)
{
options = new OutputCacheOptions();
}
if (keyProvider == null)
{
keyProvider = new OutputCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
}
return new OutputCacheMiddleware(
next,
Options.Create(options),
testSink == null ? NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true),
cache,
keyProvider);
}
internal static OutputCacheContext CreateTestContext(HttpContext? httpContext = null, IOutputCacheStore? cache = null, OutputCacheOptions? options = null, ITestSink? testSink = null)
{
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider.Setup(x => x.GetService(typeof(IOutputCacheStore))).Returns(cache ?? new SimpleTestOutputCache());
serviceProvider.Setup(x => x.GetService(typeof(IOptions<OutputCacheOptions>))).Returns(Options.Create(options ?? new OutputCacheOptions()));
serviceProvider.Setup(x => x.GetService(typeof(ILogger<OutputCacheMiddleware>))).Returns(testSink == null ? NullLogger.Instance : new TestLogger("OutputCachingTests", testSink, true));
httpContext ??= new DefaultHttpContext();
httpContext.RequestServices = serviceProvider.Object;
return new OutputCacheContext()
{
HttpContext = httpContext,
EnableOutputCaching = true,
AllowCacheStorage = true,
AllowCacheLookup = true,
ResponseTime = DateTimeOffset.UtcNow
};
}
internal static OutputCacheContext CreateUninitializedContext(HttpContext? httpContext = null, IOutputCacheStore? cache = null, OutputCacheOptions? options = null, ITestSink? testSink = null)
{
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider.Setup(x => x.GetService(typeof(IOutputCacheStore))).Returns(cache ?? new SimpleTestOutputCache());
serviceProvider.Setup(x => x.GetService(typeof(IOptions<OutputCacheOptions>))).Returns(Options.Create(options ?? new OutputCacheOptions()));
serviceProvider.Setup(x => x.GetService(typeof(ILogger<OutputCacheMiddleware>))).Returns(testSink == null ? NullLogger.Instance : new TestLogger("OutputCachingTests", testSink, true));
httpContext ??= new DefaultHttpContext();
httpContext.RequestServices = serviceProvider.Object;
return new OutputCacheContext()
{
HttpContext = httpContext,
};
}
internal static void AssertLoggedMessages(IEnumerable<WriteContext> messages, params LoggedMessage[] expectedMessages)
{
var messageList = messages.ToList();
Assert.Equal(expectedMessages.Length, messageList.Count);
for (var i = 0; i < messageList.Count; i++)
{
Assert.Equal(expectedMessages[i].EventId, messageList[i].EventId);
Assert.Equal(expectedMessages[i].LogLevel, messageList[i].LogLevel);
}
}
public static HttpRequestMessage CreateRequest(string method, string requestUri)
{
return new HttpRequestMessage(new HttpMethod(method), requestUri);
}
}
internal static class HttpResponseWritingExtensions
{
internal static void Write(this HttpResponse response, string text)
{
ArgumentNullException.ThrowIfNull(response);
ArgumentNullException.ThrowIfNull(text);
var data = Encoding.UTF8.GetBytes(text);
response.Body.Write(data, 0, data.Length);
}
}
internal class LoggedMessage
{
internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(1, LogLevel.Debug);
internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(2, LogLevel.Debug);
internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(3, LogLevel.Debug);
internal static LoggedMessage NotModifiedServed => new LoggedMessage(4, LogLevel.Information);
internal static LoggedMessage CachedResponseServed => new LoggedMessage(5, LogLevel.Information);
internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(6, LogLevel.Information);
internal static LoggedMessage NoResponseServed => new LoggedMessage(7, LogLevel.Information);
internal static LoggedMessage ResponseCached => new LoggedMessage(8, LogLevel.Information);
internal static LoggedMessage ResponseNotCached => new LoggedMessage(9, LogLevel.Information);
internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(10, LogLevel.Warning);
internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(11, LogLevel.Debug);
private LoggedMessage(int evenId, LogLevel logLevel)
{
EventId = evenId;
LogLevel = logLevel;
}
internal int EventId { get; }
internal LogLevel LogLevel { get; }
}
internal class TestResponseCachingKeyProvider : IOutputCacheKeyProvider
{
private readonly string _key;
public TestResponseCachingKeyProvider(string key)
{
_key = key;
}
public string CreateStorageKey(OutputCacheContext? context)
{
return _key;
}
}
internal class SimpleTestOutputCache : ITestOutputCacheStore
{
private readonly Dictionary<string, byte[]?> _storage = new();
public int GetCount { get; private set; }
public int SetCount { get; private set; }
private readonly object synLock = new();
public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public ValueTask<byte[]?> GetAsync(string? key, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(key);
lock (synLock)
{
GetCount++;
try
{
return ValueTask.FromResult(_storage[key]);
}
catch
{
return ValueTask.FromResult(default(byte[]));
}
}
}
public ValueTask SetAsync(string key, byte[] entry, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken)
{
lock (synLock)
{
SetCount++;
_storage[key] = entry;
return ValueTask.CompletedTask;
}
}
}
internal class BufferTestOutputCache : SimpleTestOutputCache, IOutputCacheBufferStore
{
ValueTask IOutputCacheBufferStore.SetAsync(string key, ReadOnlySequence<byte> value, ReadOnlyMemory<string> tags, TimeSpan validFor, CancellationToken cancellationToken)
=> SetAsync(key, value.ToArray(), tags.ToArray(), validFor, cancellationToken);
async ValueTask<bool> IOutputCacheBufferStore.TryGetAsync(string key, PipeWriter destination, CancellationToken cancellationToken)
{
var data = await GetAsync(key, cancellationToken); // in reality we expect this to be sync, but: meh
if (data is null)
{
return false;
}
await destination.WriteAsync(data, cancellationToken);
return true;
}
}
internal class AllowTestPolicy : IOutputCachePolicy
{
public ValueTask CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
context.AllowCacheLookup = true;
context.AllowCacheStorage = true;
return ValueTask.CompletedTask;
}
public ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
public ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
public interface ITestOutputCacheStore : IOutputCacheStore
{
int GetCount { get; }
int SetCount { get; }
}
|