Feature/dont validate cached content headers (#406)

* #372 use period timespan to decide when client can make requests again

* #400 dont validate cached body headers
This commit is contained in:
Tom Pallister 2018-06-15 20:30:25 +01:00 committed by GitHub
parent 9979f8a4b8
commit 8e1a5ce827
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 505 additions and 435 deletions

View File

@ -1,120 +1,120 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.Middleware; using Ocelot.Middleware;
using System.IO; using System.IO;
using Ocelot.Middleware.Multiplexer; using Ocelot.Middleware.Multiplexer;
namespace Ocelot.Cache.Middleware namespace Ocelot.Cache.Middleware
{ {
public class OutputCacheMiddleware : OcelotMiddleware public class OutputCacheMiddleware : OcelotMiddleware
{ {
private readonly OcelotRequestDelegate _next; private readonly OcelotRequestDelegate _next;
private readonly IOcelotCache<CachedResponse> _outputCache; private readonly IOcelotCache<CachedResponse> _outputCache;
private readonly IRegionCreator _regionCreator; private readonly IRegionCreator _regionCreator;
public OutputCacheMiddleware(OcelotRequestDelegate next, public OutputCacheMiddleware(OcelotRequestDelegate next,
IOcelotLoggerFactory loggerFactory, IOcelotLoggerFactory loggerFactory,
IOcelotCache<CachedResponse> outputCache, IOcelotCache<CachedResponse> outputCache,
IRegionCreator regionCreator) IRegionCreator regionCreator)
:base(loggerFactory.CreateLogger<OutputCacheMiddleware>()) :base(loggerFactory.CreateLogger<OutputCacheMiddleware>())
{ {
_next = next; _next = next;
_outputCache = outputCache; _outputCache = outputCache;
_regionCreator = regionCreator; _regionCreator = regionCreator;
} }
public async Task Invoke(DownstreamContext context) public async Task Invoke(DownstreamContext context)
{ {
if (!context.DownstreamReRoute.IsCached) if (!context.DownstreamReRoute.IsCached)
{ {
await _next.Invoke(context); await _next.Invoke(context);
return; return;
} }
var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"; var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}";
Logger.LogDebug($"Started checking cache for {downstreamUrlKey}"); Logger.LogDebug($"Started checking cache for {downstreamUrlKey}");
var cached = _outputCache.Get(downstreamUrlKey, context.DownstreamReRoute.CacheOptions.Region); var cached = _outputCache.Get(downstreamUrlKey, context.DownstreamReRoute.CacheOptions.Region);
if (cached != null) if (cached != null)
{ {
Logger.LogDebug($"cache entry exists for {downstreamUrlKey}"); Logger.LogDebug($"cache entry exists for {downstreamUrlKey}");
var response = CreateHttpResponseMessage(cached); var response = CreateHttpResponseMessage(cached);
SetHttpResponseMessageThisRequest(context, response); SetHttpResponseMessageThisRequest(context, response);
Logger.LogDebug($"finished returned cached response for {downstreamUrlKey}"); Logger.LogDebug($"finished returned cached response for {downstreamUrlKey}");
return; return;
} }
Logger.LogDebug($"no resonse cached for {downstreamUrlKey}"); Logger.LogDebug($"no resonse cached for {downstreamUrlKey}");
await _next.Invoke(context); await _next.Invoke(context);
if (context.IsError) if (context.IsError)
{ {
Logger.LogDebug($"there was a pipeline error for {downstreamUrlKey}"); Logger.LogDebug($"there was a pipeline error for {downstreamUrlKey}");
return; return;
} }
cached = await CreateCachedResponse(context.DownstreamResponse); cached = await CreateCachedResponse(context.DownstreamResponse);
_outputCache.Add(downstreamUrlKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region); _outputCache.Add(downstreamUrlKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region);
Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}"); Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}");
} }
private void SetHttpResponseMessageThisRequest(DownstreamContext context, DownstreamResponse response) private void SetHttpResponseMessageThisRequest(DownstreamContext context, DownstreamResponse response)
{ {
context.DownstreamResponse = response; context.DownstreamResponse = response;
} }
internal DownstreamResponse CreateHttpResponseMessage(CachedResponse cached) internal DownstreamResponse CreateHttpResponseMessage(CachedResponse cached)
{ {
if (cached == null) if (cached == null)
{ {
return null; return null;
} }
var content = new MemoryStream(Convert.FromBase64String(cached.Body)); var content = new MemoryStream(Convert.FromBase64String(cached.Body));
var streamContent = new StreamContent(content); var streamContent = new StreamContent(content);
foreach (var header in cached.ContentHeaders) foreach (var header in cached.ContentHeaders)
{ {
streamContent.Headers.Add(header.Key, header.Value); streamContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
} }
return new DownstreamResponse(streamContent, cached.StatusCode, cached.Headers.ToList()); return new DownstreamResponse(streamContent, cached.StatusCode, cached.Headers.ToList());
} }
internal async Task<CachedResponse> CreateCachedResponse(DownstreamResponse response) internal async Task<CachedResponse> CreateCachedResponse(DownstreamResponse response)
{ {
if (response == null) if (response == null)
{ {
return null; return null;
} }
var statusCode = response.StatusCode; var statusCode = response.StatusCode;
var headers = response.Headers.ToDictionary(v => v.Key, v => v.Values); var headers = response.Headers.ToDictionary(v => v.Key, v => v.Values);
string body = null; string body = null;
if (response.Content != null) if (response.Content != null)
{ {
var content = await response.Content.ReadAsByteArrayAsync(); var content = await response.Content.ReadAsByteArrayAsync();
body = Convert.ToBase64String(content); body = Convert.ToBase64String(content);
} }
var contentHeaders = response?.Content?.Headers.ToDictionary(v => v.Key, v => v.Value); var contentHeaders = response?.Content?.Headers.ToDictionary(v => v.Key, v => v.Value);
var cached = new CachedResponse(statusCode, headers, body, contentHeaders); var cached = new CachedResponse(statusCode, headers, body, contentHeaders);
return cached; return cached;
} }
} }
} }

View File

@ -1,191 +1,239 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
namespace Ocelot.AcceptanceTests namespace Ocelot.AcceptanceTests
{ {
public class CachingTests : IDisposable public class CachingTests : IDisposable
{ {
private IWebHost _builder; private IWebHost _builder;
private readonly Steps _steps; private readonly Steps _steps;
public CachingTests() public CachingTests()
{ {
_steps = new Steps(); _steps = new Steps();
} }
[Fact] [Fact]
public void should_return_cached_response() public void should_return_cached_response()
{ {
var configuration = new FileConfiguration var configuration = new FileConfiguration
{ {
ReRoutes = new List<FileReRoute> ReRoutes = new List<FileReRoute>
{ {
new FileReRoute new FileReRoute
{ {
DownstreamPathTemplate = "/", DownstreamPathTemplate = "/",
DownstreamHostAndPorts = new List<FileHostAndPort> DownstreamHostAndPorts = new List<FileHostAndPort>
{ {
new FileHostAndPort new FileHostAndPort
{ {
Host = "localhost", Host = "localhost",
Port = 51899, Port = 51899,
} }
}, },
DownstreamScheme = "http", DownstreamScheme = "http",
UpstreamPathTemplate = "/", UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" }, UpstreamHttpMethod = new List<string> { "Get" },
FileCacheOptions = new FileCacheOptions FileCacheOptions = new FileCacheOptions
{ {
TtlSeconds = 100 TtlSeconds = 100
} }
} }
} }
}; };
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura")) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom"))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.And(x => _steps.ThenTheContentLengthIs(16)) .And(x => _steps.ThenTheContentLengthIs(16))
.BDDfy(); .BDDfy();
} }
[Fact] [Fact]
public void should_return_cached_response_when_using_jsonserialized_cache() public void should_return_cached_response_with_expires_header()
{ {
var configuration = new FileConfiguration var configuration = new FileConfiguration
{ {
ReRoutes = new List<FileReRoute> ReRoutes = new List<FileReRoute>
{ {
new FileReRoute new FileReRoute
{ {
DownstreamPathTemplate = "/", DownstreamPathTemplate = "/",
DownstreamHostAndPorts = new List<FileHostAndPort> DownstreamHostAndPorts = new List<FileHostAndPort>
{ {
new FileHostAndPort new FileHostAndPort
{ {
Host = "localhost", Host = "localhost",
Port = 51899, Port = 52839,
} }
}, },
DownstreamScheme = "http", DownstreamScheme = "http",
UpstreamPathTemplate = "/", UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" }, UpstreamHttpMethod = new List<string> { "Get" },
FileCacheOptions = new FileCacheOptions FileCacheOptions = new FileCacheOptions
{ {
TtlSeconds = 100 TtlSeconds = 100
} }
} }
} }
}; };
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura")) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52839", 200, "Hello from Laura", "Expires", "-1"))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunningUsingJsonSerializedCache()) .And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) .Given(x => x.GivenTheServiceNowReturns("http://localhost:52839", 200, "Hello from Tom"))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.BDDfy(); .And(x => _steps.ThenTheContentLengthIs(16))
} .And(x => _steps.ThenTheResponseBodyHeaderIs("Expires", "-1"))
.BDDfy();
[Fact] }
public void should_not_return_cached_response_as_ttl_expires()
{ [Fact]
var configuration = new FileConfiguration public void should_return_cached_response_when_using_jsonserialized_cache()
{ {
ReRoutes = new List<FileReRoute> var configuration = new FileConfiguration
{ {
new FileReRoute ReRoutes = new List<FileReRoute>
{ {
DownstreamPathTemplate = "/", new FileReRoute
DownstreamHostAndPorts = new List<FileHostAndPort> {
{ DownstreamPathTemplate = "/",
new FileHostAndPort DownstreamHostAndPorts = new List<FileHostAndPort>
{ {
Host = "localhost", new FileHostAndPort
Port = 51899, {
} Host = "localhost",
}, Port = 51899,
DownstreamScheme = "http", }
UpstreamPathTemplate = "/", },
UpstreamHttpMethod = new List<string> { "Get" }, DownstreamScheme = "http",
FileCacheOptions = new FileCacheOptions UpstreamPathTemplate = "/",
{ UpstreamHttpMethod = new List<string> { "Get" },
TtlSeconds = 1 FileCacheOptions = new FileCacheOptions
} {
} TtlSeconds = 100
} }
}; }
}
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura")) };
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.GivenOcelotIsRunningUsingJsonSerializedCache())
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => x.GivenTheCacheExpires()) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.BDDfy(); .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
} .BDDfy();
}
private void GivenTheCacheExpires()
{ [Fact]
Thread.Sleep(1000); public void should_not_return_cached_response_as_ttl_expires()
} {
var configuration = new FileConfiguration
private void GivenTheServiceNowReturns(string url, int statusCode, string responseBody) {
{ ReRoutes = new List<FileReRoute>
_builder.Dispose(); {
GivenThereIsAServiceRunningOn(url, statusCode, responseBody); new FileReRoute
} {
DownstreamPathTemplate = "/",
private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) DownstreamHostAndPorts = new List<FileHostAndPort>
{ {
_builder = new WebHostBuilder() new FileHostAndPort
.UseUrls(url) {
.UseKestrel() Host = "localhost",
.UseContentRoot(Directory.GetCurrentDirectory()) Port = 51899,
.UseIISIntegration() }
.UseUrls(url) },
.Configure(app => DownstreamScheme = "http",
{ UpstreamPathTemplate = "/",
app.Run(async context => UpstreamHttpMethod = new List<string> { "Get" },
{ FileCacheOptions = new FileCacheOptions
context.Response.StatusCode = statusCode; {
await context.Response.WriteAsync(responseBody); TtlSeconds = 1
}); }
}) }
.Build(); }
};
_builder.Start();
} this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
public void Dispose() .And(x => _steps.GivenOcelotIsRunning())
{ .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
_builder?.Dispose(); .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
_steps.Dispose(); .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
} .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom"))
} .And(x => x.GivenTheCacheExpires())
} .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom"))
.BDDfy();
}
private void GivenTheCacheExpires()
{
Thread.Sleep(1000);
}
private void GivenTheServiceNowReturns(string url, int statusCode, string responseBody)
{
_builder.Dispose();
GivenThereIsAServiceRunningOn(url, statusCode, responseBody, null, null);
}
private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, string key, string value)
{
_builder = new WebHostBuilder()
.UseUrls(url)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls(url)
.Configure(app =>
{
app.Run(async context =>
{
if(!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(key))
{
context.Response.Headers.Add(key, value);
}
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(responseBody);
});
})
.Build();
_builder.Start();
}
public void Dispose()
{
_builder?.Dispose();
_steps.Dispose();
}
}
}

View File

@ -392,6 +392,12 @@ namespace Ocelot.AcceptanceTests
header.First().ShouldBe(value); header.First().ShouldBe(value);
} }
public void ThenTheResponseBodyHeaderIs(string key, string value)
{
var header = _response.Content.Headers.GetValues(key);
header.First().ShouldBe(value);
}
public void ThenTheTraceHeaderIsSet(string key) public void ThenTheTraceHeaderIsSet(string key)
{ {
var header = _response.Headers.GetValues(key); var header = _response.Headers.GetValues(key);

View File

@ -1,124 +1,140 @@
namespace Ocelot.UnitTests.Cache namespace Ocelot.UnitTests.Cache
{ {
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Moq; using Moq;
using Ocelot.Cache; using Ocelot.Cache;
using Ocelot.Cache.Middleware; using Ocelot.Cache.Middleware;
using Ocelot.Configuration; using Ocelot.Configuration;
using Ocelot.Configuration.Builder; using Ocelot.Configuration.Builder;
using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder;
using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.Logging; using Ocelot.Logging;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
using System.Net; using System.Net;
using Ocelot.Middleware; using Ocelot.Middleware;
using Ocelot.Middleware.Multiplexer; using Ocelot.Middleware.Multiplexer;
public class OutputCacheMiddlewareTests public class OutputCacheMiddlewareTests
{ {
private readonly Mock<IOcelotCache<CachedResponse>> _cacheManager; private readonly Mock<IOcelotCache<CachedResponse>> _cacheManager;
private readonly Mock<IOcelotLoggerFactory> _loggerFactory; private readonly Mock<IOcelotLoggerFactory> _loggerFactory;
private Mock<IOcelotLogger> _logger; private Mock<IOcelotLogger> _logger;
private OutputCacheMiddleware _middleware; private OutputCacheMiddleware _middleware;
private readonly DownstreamContext _downstreamContext; private readonly DownstreamContext _downstreamContext;
private readonly OcelotRequestDelegate _next; private readonly OcelotRequestDelegate _next;
private CachedResponse _response; private CachedResponse _response;
private readonly IRegionCreator _regionCreator; private readonly IRegionCreator _regionCreator;
public OutputCacheMiddlewareTests() public OutputCacheMiddlewareTests()
{ {
_cacheManager = new Mock<IOcelotCache<CachedResponse>>(); _cacheManager = new Mock<IOcelotCache<CachedResponse>>();
_regionCreator = new RegionCreator(); _regionCreator = new RegionCreator();
_downstreamContext = new DownstreamContext(new DefaultHttpContext()); _downstreamContext = new DownstreamContext(new DefaultHttpContext());
_loggerFactory = new Mock<IOcelotLoggerFactory>(); _loggerFactory = new Mock<IOcelotLoggerFactory>();
_logger = new Mock<IOcelotLogger>(); _logger = new Mock<IOcelotLogger>();
_loggerFactory.Setup(x => x.CreateLogger<OutputCacheMiddleware>()).Returns(_logger.Object); _loggerFactory.Setup(x => x.CreateLogger<OutputCacheMiddleware>()).Returns(_logger.Object);
_next = context => Task.CompletedTask; _next = context => Task.CompletedTask;
_downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"));
} }
[Fact] [Fact]
public void should_returned_cached_item_when_it_is_in_cache() public void should_returned_cached_item_when_it_is_in_cache()
{ {
var headers = new Dictionary<string, IEnumerable<string>> var headers = new Dictionary<string, IEnumerable<string>>
{ {
{ "test", new List<string> { "test" } } { "test", new List<string> { "test" } }
}; };
var contentHeaders = new Dictionary<string, IEnumerable<string>> var contentHeaders = new Dictionary<string, IEnumerable<string>>
{ {
{ "content-type", new List<string> { "application/json" } } { "content-type", new List<string> { "application/json" } }
}; };
var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, "", contentHeaders); var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, "", contentHeaders);
this.Given(x => x.GivenThereIsACachedResponse(cachedResponse)) this.Given(x => x.GivenThereIsACachedResponse(cachedResponse))
.And(x => x.GivenTheDownstreamRouteIs()) .And(x => x.GivenTheDownstreamRouteIs())
.When(x => x.WhenICallTheMiddleware()) .When(x => x.WhenICallTheMiddleware())
.Then(x => x.ThenTheCacheGetIsCalledCorrectly()) .Then(x => x.ThenTheCacheGetIsCalledCorrectly())
.BDDfy(); .BDDfy();
} }
[Fact] [Fact]
public void should_continue_with_pipeline_and_cache_response() public void should_returned_cached_item_when_it_is_in_cache_expires_header()
{ {
this.Given(x => x.GivenResponseIsNotCached()) var contentHeaders = new Dictionary<string, IEnumerable<string>>
.And(x => x.GivenTheDownstreamRouteIs()) {
.When(x => x.WhenICallTheMiddleware()) { "Expires", new List<string> { "-1" } }
.Then(x => x.ThenTheCacheAddIsCalledCorrectly()) };
.BDDfy();
} var cachedResponse = new CachedResponse(HttpStatusCode.OK, new Dictionary<string, IEnumerable<string>>(), "", contentHeaders);
this.Given(x => x.GivenThereIsACachedResponse(cachedResponse))
private void WhenICallTheMiddleware() .And(x => x.GivenTheDownstreamRouteIs())
{ .When(x => x.WhenICallTheMiddleware())
_middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager.Object, _regionCreator); .Then(x => x.ThenTheCacheGetIsCalledCorrectly())
_middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); .BDDfy();
} }
private void GivenThereIsACachedResponse(CachedResponse response) [Fact]
{ public void should_continue_with_pipeline_and_cache_response()
_response = response; {
_cacheManager this.Given(x => x.GivenResponseIsNotCached(new HttpResponseMessage()))
.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>())) .And(x => x.GivenTheDownstreamRouteIs())
.Returns(_response); .When(x => x.WhenICallTheMiddleware())
} .Then(x => x.ThenTheCacheAddIsCalledCorrectly())
.BDDfy();
private void GivenResponseIsNotCached() }
{
_downstreamContext.DownstreamResponse = new DownstreamResponse(new HttpResponseMessage()); private void WhenICallTheMiddleware()
} {
_middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager.Object, _regionCreator);
private void GivenTheDownstreamRouteIs() _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult();
{ }
var reRoute = new ReRouteBuilder()
.WithDownstreamReRoute(new DownstreamReRouteBuilder() private void GivenThereIsACachedResponse(CachedResponse response)
.WithIsCached(true) {
.WithCacheOptions(new CacheOptions(100, "kanken")) _response = response;
.WithUpstreamHttpMethod(new List<string> { "Get" }) _cacheManager
.Build()) .Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>()))
.WithUpstreamHttpMethod(new List<string> { "Get" }) .Returns(_response);
.Build(); }
var downstreamRoute = new DownstreamRoute(new List<PlaceholderNameAndValue>(), reRoute); private void GivenResponseIsNotCached(HttpResponseMessage responseMessage)
{
_downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; _downstreamContext.DownstreamResponse = new DownstreamResponse(responseMessage);
_downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; }
}
private void GivenTheDownstreamRouteIs()
private void ThenTheCacheGetIsCalledCorrectly() {
{ var reRoute = new ReRouteBuilder()
_cacheManager .WithDownstreamReRoute(new DownstreamReRouteBuilder()
.Verify(x => x.Get(It.IsAny<string>(), It.IsAny<string>()), Times.Once); .WithIsCached(true)
} .WithCacheOptions(new CacheOptions(100, "kanken"))
.WithUpstreamHttpMethod(new List<string> { "Get" })
private void ThenTheCacheAddIsCalledCorrectly() .Build())
{ .WithUpstreamHttpMethod(new List<string> { "Get" })
_cacheManager .Build();
.Verify(x => x.Add(It.IsAny<string>(), It.IsAny<CachedResponse>(), It.IsAny<TimeSpan>(), It.IsAny<string>()), Times.Once);
} var downstreamRoute = new DownstreamRoute(new List<PlaceholderNameAndValue>(), reRoute);
}
} _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues;
_downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0];
}
private void ThenTheCacheGetIsCalledCorrectly()
{
_cacheManager
.Verify(x => x.Get(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
private void ThenTheCacheAddIsCalledCorrectly()
{
_cacheManager
.Verify(x => x.Add(It.IsAny<string>(), It.IsAny<CachedResponse>(), It.IsAny<TimeSpan>(), It.IsAny<string>()), Times.Once);
}
}
}