diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs index aee3bec3..8a27ccba 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -1,120 +1,120 @@ -using System; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Ocelot.Logging; -using Ocelot.Middleware; -using System.IO; -using Ocelot.Middleware.Multiplexer; - -namespace Ocelot.Cache.Middleware -{ - public class OutputCacheMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IOcelotCache _outputCache; - private readonly IRegionCreator _regionCreator; - - public OutputCacheMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IOcelotCache outputCache, - IRegionCreator regionCreator) - :base(loggerFactory.CreateLogger()) - { - _next = next; - _outputCache = outputCache; - _regionCreator = regionCreator; - } - - public async Task Invoke(DownstreamContext context) - { - if (!context.DownstreamReRoute.IsCached) - { - await _next.Invoke(context); - return; - } - - var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"; - - Logger.LogDebug($"Started checking cache for {downstreamUrlKey}"); - - var cached = _outputCache.Get(downstreamUrlKey, context.DownstreamReRoute.CacheOptions.Region); - - if (cached != null) - { - Logger.LogDebug($"cache entry exists for {downstreamUrlKey}"); - - var response = CreateHttpResponseMessage(cached); - SetHttpResponseMessageThisRequest(context, response); - - Logger.LogDebug($"finished returned cached response for {downstreamUrlKey}"); - - return; - } - - Logger.LogDebug($"no resonse cached for {downstreamUrlKey}"); - - await _next.Invoke(context); - - if (context.IsError) - { - Logger.LogDebug($"there was a pipeline error for {downstreamUrlKey}"); - - return; - } - - cached = await CreateCachedResponse(context.DownstreamResponse); - - _outputCache.Add(downstreamUrlKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region); - - Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}"); - } - - private void SetHttpResponseMessageThisRequest(DownstreamContext context, DownstreamResponse response) - { - context.DownstreamResponse = response; - } - - internal DownstreamResponse CreateHttpResponseMessage(CachedResponse cached) - { - if (cached == null) - { - return null; - } - - var content = new MemoryStream(Convert.FromBase64String(cached.Body)); - - var streamContent = new StreamContent(content); - - foreach (var header in cached.ContentHeaders) - { - streamContent.Headers.Add(header.Key, header.Value); - } - - return new DownstreamResponse(streamContent, cached.StatusCode, cached.Headers.ToList()); - } - - internal async Task CreateCachedResponse(DownstreamResponse response) - { - if (response == null) - { - return null; - } - - var statusCode = response.StatusCode; - var headers = response.Headers.ToDictionary(v => v.Key, v => v.Values); - string body = null; - - if (response.Content != null) - { - var content = await response.Content.ReadAsByteArrayAsync(); - body = Convert.ToBase64String(content); - } - - var contentHeaders = response?.Content?.Headers.ToDictionary(v => v.Key, v => v.Value); - - var cached = new CachedResponse(statusCode, headers, body, contentHeaders); - return cached; - } - } -} +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Ocelot.Logging; +using Ocelot.Middleware; +using System.IO; +using Ocelot.Middleware.Multiplexer; + +namespace Ocelot.Cache.Middleware +{ + public class OutputCacheMiddleware : OcelotMiddleware + { + private readonly OcelotRequestDelegate _next; + private readonly IOcelotCache _outputCache; + private readonly IRegionCreator _regionCreator; + + public OutputCacheMiddleware(OcelotRequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IOcelotCache outputCache, + IRegionCreator regionCreator) + :base(loggerFactory.CreateLogger()) + { + _next = next; + _outputCache = outputCache; + _regionCreator = regionCreator; + } + + public async Task Invoke(DownstreamContext context) + { + if (!context.DownstreamReRoute.IsCached) + { + await _next.Invoke(context); + return; + } + + var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"; + + Logger.LogDebug($"Started checking cache for {downstreamUrlKey}"); + + var cached = _outputCache.Get(downstreamUrlKey, context.DownstreamReRoute.CacheOptions.Region); + + if (cached != null) + { + Logger.LogDebug($"cache entry exists for {downstreamUrlKey}"); + + var response = CreateHttpResponseMessage(cached); + SetHttpResponseMessageThisRequest(context, response); + + Logger.LogDebug($"finished returned cached response for {downstreamUrlKey}"); + + return; + } + + Logger.LogDebug($"no resonse cached for {downstreamUrlKey}"); + + await _next.Invoke(context); + + if (context.IsError) + { + Logger.LogDebug($"there was a pipeline error for {downstreamUrlKey}"); + + return; + } + + cached = await CreateCachedResponse(context.DownstreamResponse); + + _outputCache.Add(downstreamUrlKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region); + + Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}"); + } + + private void SetHttpResponseMessageThisRequest(DownstreamContext context, DownstreamResponse response) + { + context.DownstreamResponse = response; + } + + internal DownstreamResponse CreateHttpResponseMessage(CachedResponse cached) + { + if (cached == null) + { + return null; + } + + var content = new MemoryStream(Convert.FromBase64String(cached.Body)); + + var streamContent = new StreamContent(content); + + foreach (var header in cached.ContentHeaders) + { + streamContent.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return new DownstreamResponse(streamContent, cached.StatusCode, cached.Headers.ToList()); + } + + internal async Task CreateCachedResponse(DownstreamResponse response) + { + if (response == null) + { + return null; + } + + var statusCode = response.StatusCode; + var headers = response.Headers.ToDictionary(v => v.Key, v => v.Values); + string body = null; + + if (response.Content != null) + { + var content = await response.Content.ReadAsByteArrayAsync(); + body = Convert.ToBase64String(content); + } + + var contentHeaders = response?.Content?.Headers.ToDictionary(v => v.Key, v => v.Value); + + var cached = new CachedResponse(statusCode, headers, body, contentHeaders); + return cached; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/CachingTests.cs b/test/Ocelot.AcceptanceTests/CachingTests.cs index f64e2ce2..ad2a12b8 100644 --- a/test/Ocelot.AcceptanceTests/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/CachingTests.cs @@ -1,191 +1,239 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Threading; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.AcceptanceTests -{ - public class CachingTests : IDisposable - { - private IWebHost _builder; - private readonly Steps _steps; - - public CachingTests() - { - _steps = new Steps(); - } - - [Fact] - public void should_return_cached_response() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51899, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - 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()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.ThenTheContentLengthIs(16)) - .BDDfy(); - } - - [Fact] - public void should_return_cached_response_when_using_jsonserialized_cache() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51899, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - 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.GivenOcelotIsRunningUsingJsonSerializedCache()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_not_return_cached_response_as_ttl_expires() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51899, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 1 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .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); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) - { - _builder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - }) - .Build(); - - _builder.Start(); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class CachingTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + + public CachingTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_cached_response() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51899, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 100 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheContentLengthIs(16)) + .BDDfy(); + } + + [Fact] + public void should_return_cached_response_with_expires_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 52839, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 100 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52839", 200, "Hello from Laura", "Expires", "-1")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => x.GivenTheServiceNowReturns("http://localhost:52839", 200, "Hello from Tom")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheContentLengthIs(16)) + .And(x => _steps.ThenTheResponseBodyHeaderIs("Expires", "-1")) + .BDDfy(); + } + + [Fact] + public void should_return_cached_response_when_using_jsonserialized_cache() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51899, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 100 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingJsonSerializedCache()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_not_return_cached_response_as_ttl_expires() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51899, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 1 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .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(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 67723ccb..5dd95831 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -392,6 +392,12 @@ namespace Ocelot.AcceptanceTests 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) { var header = _response.Headers.GetValues(key); diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index cd0f9719..c0b59350 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -1,124 +1,140 @@ -namespace Ocelot.UnitTests.Cache -{ - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Cache; - using Ocelot.Cache.Middleware; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.Logging; - using TestStack.BDDfy; - using Xunit; - using System.Net; - using Ocelot.Middleware; - using Ocelot.Middleware.Multiplexer; - - public class OutputCacheMiddlewareTests - { - private readonly Mock> _cacheManager; - private readonly Mock _loggerFactory; - private Mock _logger; - private OutputCacheMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private readonly OcelotRequestDelegate _next; - private CachedResponse _response; - private readonly IRegionCreator _regionCreator; - - public OutputCacheMiddlewareTests() - { - _cacheManager = new Mock>(); - _regionCreator = new RegionCreator(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); - } - - [Fact] - public void should_returned_cached_item_when_it_is_in_cache() - { - var headers = new Dictionary> - { - { "test", new List { "test" } } - }; - - var contentHeaders = new Dictionary> - { - { "content-type", new List { "application/json" } } - }; - - var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, "", contentHeaders); - this.Given(x => x.GivenThereIsACachedResponse(cachedResponse)) - .And(x => x.GivenTheDownstreamRouteIs()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheCacheGetIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_continue_with_pipeline_and_cache_response() - { - this.Given(x => x.GivenResponseIsNotCached()) - .And(x => x.GivenTheDownstreamRouteIs()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheCacheAddIsCalledCorrectly()) - .BDDfy(); - } - - private void WhenICallTheMiddleware() - { - _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager.Object, _regionCreator); - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } - - private void GivenThereIsACachedResponse(CachedResponse response) - { - _response = response; - _cacheManager - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .Returns(_response); - } - - private void GivenResponseIsNotCached() - { - _downstreamContext.DownstreamResponse = new DownstreamResponse(new HttpResponseMessage()); - } - - private void GivenTheDownstreamRouteIs() - { - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken")) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - var downstreamRoute = new DownstreamRoute(new List(), reRoute); - - _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; - } - - private void ThenTheCacheGetIsCalledCorrectly() - { - _cacheManager - .Verify(x => x.Get(It.IsAny(), It.IsAny()), Times.Once); - } - - private void ThenTheCacheAddIsCalledCorrectly() - { - _cacheManager - .Verify(x => x.Add(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } - } -} +namespace Ocelot.UnitTests.Cache +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + using Moq; + using Ocelot.Cache; + using Ocelot.Cache.Middleware; + using Ocelot.Configuration; + using Ocelot.Configuration.Builder; + using Ocelot.DownstreamRouteFinder; + using Ocelot.DownstreamRouteFinder.UrlMatcher; + using Ocelot.Logging; + using TestStack.BDDfy; + using Xunit; + using System.Net; + using Ocelot.Middleware; + using Ocelot.Middleware.Multiplexer; + + public class OutputCacheMiddlewareTests + { + private readonly Mock> _cacheManager; + private readonly Mock _loggerFactory; + private Mock _logger; + private OutputCacheMiddleware _middleware; + private readonly DownstreamContext _downstreamContext; + private readonly OcelotRequestDelegate _next; + private CachedResponse _response; + private readonly IRegionCreator _regionCreator; + + public OutputCacheMiddlewareTests() + { + _cacheManager = new Mock>(); + _regionCreator = new RegionCreator(); + _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); + } + + [Fact] + public void should_returned_cached_item_when_it_is_in_cache() + { + var headers = new Dictionary> + { + { "test", new List { "test" } } + }; + + var contentHeaders = new Dictionary> + { + { "content-type", new List { "application/json" } } + }; + + var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, "", contentHeaders); + this.Given(x => x.GivenThereIsACachedResponse(cachedResponse)) + .And(x => x.GivenTheDownstreamRouteIs()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheCacheGetIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_returned_cached_item_when_it_is_in_cache_expires_header() + { + var contentHeaders = new Dictionary> + { + { "Expires", new List { "-1" } } + }; + + var cachedResponse = new CachedResponse(HttpStatusCode.OK, new Dictionary>(), "", contentHeaders); + this.Given(x => x.GivenThereIsACachedResponse(cachedResponse)) + .And(x => x.GivenTheDownstreamRouteIs()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheCacheGetIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_continue_with_pipeline_and_cache_response() + { + this.Given(x => x.GivenResponseIsNotCached(new HttpResponseMessage())) + .And(x => x.GivenTheDownstreamRouteIs()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheCacheAddIsCalledCorrectly()) + .BDDfy(); + } + + private void WhenICallTheMiddleware() + { + _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager.Object, _regionCreator); + _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + } + + private void GivenThereIsACachedResponse(CachedResponse response) + { + _response = response; + _cacheManager + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .Returns(_response); + } + + private void GivenResponseIsNotCached(HttpResponseMessage responseMessage) + { + _downstreamContext.DownstreamResponse = new DownstreamResponse(responseMessage); + } + + private void GivenTheDownstreamRouteIs() + { + var reRoute = new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithIsCached(true) + .WithCacheOptions(new CacheOptions(100, "kanken")) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build(); + + var downstreamRoute = new DownstreamRoute(new List(), reRoute); + + _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; + _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; + } + + private void ThenTheCacheGetIsCalledCorrectly() + { + _cacheManager + .Verify(x => x.Get(It.IsAny(), It.IsAny()), Times.Once); + } + + private void ThenTheCacheAddIsCalledCorrectly() + { + _cacheManager + .Verify(x => x.Add(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + } +}