using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using Ocelot.Middleware; using Ocelot.Middleware.Multiplexer; using Shouldly; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using TestStack.BDDfy; using Xunit; namespace Ocelot.AcceptanceTests { public class AggregateTests : IDisposable { private readonly Steps _steps; private string _downstreamPathOne; private string _downstreamPathTwo; private readonly ServiceHandler _serviceHandler; public AggregateTests() { _serviceHandler = new ServiceHandler(); _steps = new Steps(); } [Fact] public void should_fix_issue_597() { var configuration = new FileConfiguration { ReRoutes = new List { new FileReRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key1data/{userid}", UpstreamHttpMethod = new List {"Get"}, DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 8571 } }, Key = "key1" }, new FileReRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key2data/{userid}", UpstreamHttpMethod = new List {"Get"}, DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 8571 } }, Key = "key2" }, new FileReRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key3data/{userid}", UpstreamHttpMethod = new List {"Get"}, DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 8571 } }, Key = "key3" }, new FileReRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key4data/{userid}", UpstreamHttpMethod = new List {"Get"}, DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 8571 } }, Key = "key4" }, }, Aggregates = new List { new FileAggregateReRoute { ReRouteKeys = new List{ "key1", "key2", "key3", "key4" }, UpstreamPathTemplate = "/EmpDetail/IN/{userid}" }, new FileAggregateReRoute { ReRouteKeys = new List{ "key1", "key2", }, UpstreamPathTemplate = "/EmpDetail/US/{userid}" } }, GlobalConfiguration = new FileGlobalConfiguration { RequestIdKey = "CorrelationID" } }; var expected = "{\"key1\":some_data,\"key2\":some_data}"; this.Given(x => x.GivenServiceIsRunning("http://localhost:8571", 200, "some_data")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EmpDetail/US/1")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } [Fact] public void should_return_response_200_with_advanced_aggregate_configs() { var configuration = new FileConfiguration { ReRoutes = new List { new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51889, } }, UpstreamPathTemplate = "/Comments", UpstreamHttpMethod = new List { "Get" }, Key = "Comments" }, new FileReRoute { DownstreamPathTemplate = "/users/{userId}", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 54030, } }, UpstreamPathTemplate = "/UserDetails", UpstreamHttpMethod = new List { "Get" }, Key = "UserDetails" }, new FileReRoute { DownstreamPathTemplate = "/posts/{postId}", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51887, } }, UpstreamPathTemplate = "/PostDetails", UpstreamHttpMethod = new List { "Get" }, Key = "PostDetails" } }, Aggregates = new List { new FileAggregateReRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", ReRouteKeys = new List { "Comments", "UserDetails", "PostDetails" }, ReRouteKeysConfig = new List() { new AggregateReRouteConfig(){ReRouteKey = "UserDetails",JsonPath = "$[*].writerId",Parameter = "userId"}, new AggregateReRouteConfig(){ReRouteKey = "PostDetails",JsonPath = "$[*].postId",Parameter = "postId"} }, } } }; var userDetailsResponseContent = @"{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""}"; var postDetailsResponseContent = @"{""id"":1,""title"":""post1""}"; var commentsResponseContent = @"[{""id"":1,""writerId"":1,""postId"":2,""text"":""text1""},{""id"":2,""writerId"":1,""postId"":2,""text"":""text2""}]"; var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + ",\"PostDetails\":" + postDetailsResponseContent + "}"; this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51889", "/", 200, commentsResponseContent)) .Given(x => x.GivenServiceTwoIsRunning("http://localhost:54030", "/users/1", 200, userDetailsResponseContent)) .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51887", "/posts/2", 200, postDetailsResponseContent)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } [Fact] public void should_return_response_200_with_simple_url_user_defined_aggregate() { var configuration = new FileConfiguration { ReRoutes = new List { new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51885, } }, UpstreamPathTemplate = "/laura", UpstreamHttpMethod = new List { "Get" }, Key = "Laura" }, new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51886, } }, UpstreamPathTemplate = "/tom", UpstreamHttpMethod = new List { "Get" }, Key = "Tom" } }, Aggregates = new List { new FileAggregateReRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", ReRouteKeys = new List { "Laura", "Tom" }, Aggregator = "FakeDefinedAggregator" } } }; var expected = "Bye from Laura, Bye from Tom"; this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51885", "/", 200, "{Hello from Laura}")) .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51886", "/", 200, "{Hello from Tom}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithSpecficAggregatorsRegisteredInDi()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] public void should_return_response_200_with_simple_url() { var configuration = new FileConfiguration { ReRoutes = new List { new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51875, } }, UpstreamPathTemplate = "/laura", UpstreamHttpMethod = new List { "Get" }, Key = "Laura" }, new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51886, } }, UpstreamPathTemplate = "/tom", UpstreamHttpMethod = new List { "Get" }, Key = "Tom" } }, Aggregates = new List { new FileAggregateReRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", ReRouteKeys = new List { "Laura", "Tom" } } } }; var expected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51875", "/", 200, "{Hello from Laura}")) .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51886", "/", 200, "{Hello from Tom}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] public void should_return_response_200_with_simple_url_one_service_404() { var configuration = new FileConfiguration { ReRoutes = new List { new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51881, } }, UpstreamPathTemplate = "/laura", UpstreamHttpMethod = new List { "Get" }, Key = "Laura" }, new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51889, } }, UpstreamPathTemplate = "/tom", UpstreamHttpMethod = new List { "Get" }, Key = "Tom" } }, Aggregates = new List { new FileAggregateReRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", ReRouteKeys = new List { "Laura", "Tom" } } } }; var expected = "{\"Laura\":,\"Tom\":{Hello from Tom}}"; this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51881", "/", 404, "")) .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51889", "/", 200, "{Hello from Tom}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] public void should_return_response_200_with_simple_url_both_service_404() { var configuration = new FileConfiguration { ReRoutes = new List { new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51883, } }, UpstreamPathTemplate = "/laura", UpstreamHttpMethod = new List { "Get" }, Key = "Laura" }, new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51884, } }, UpstreamPathTemplate = "/tom", UpstreamHttpMethod = new List { "Get" }, Key = "Tom" } }, Aggregates = new List { new FileAggregateReRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", ReRouteKeys = new List { "Laura", "Tom" } } } }; var expected = "{\"Laura\":,\"Tom\":}"; this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51883", "/", 404, "")) .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51884", "/", 404, "")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] public void should_be_thread_safe() { var configuration = new FileConfiguration { ReRoutes = new List { new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51878, } }, UpstreamPathTemplate = "/laura", UpstreamHttpMethod = new List { "Get" }, Key = "Laura" }, new FileReRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new FileHostAndPort { Host = "localhost", Port = 51880, } }, UpstreamPathTemplate = "/tom", UpstreamHttpMethod = new List { "Get" }, Key = "Tom" } }, Aggregates = new List { new FileAggregateReRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", ReRouteKeys = new List { "Laura", "Tom" } } } }; this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51878", "/", 200, "{Hello from Laura}")) .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51880", "/", 200, "{Hello from Tom}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIMakeLotsOfDifferentRequestsToTheApiGateway()) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } private void GivenServiceIsRunning(string baseUrl, int statusCode, string responseBody) { _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => { context.Response.StatusCode = statusCode; await context.Response.WriteAsync(responseBody); }); } private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) { _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => { _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; if (_downstreamPathOne != basePath) { context.Response.StatusCode = statusCode; await context.Response.WriteAsync("downstream path didnt match base path"); } else { context.Response.StatusCode = statusCode; await context.Response.WriteAsync(responseBody); } }); } private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) { _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => { _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; if (_downstreamPathTwo != basePath) { context.Response.StatusCode = statusCode; await context.Response.WriteAsync("downstream path didnt match base path"); } else { context.Response.StatusCode = statusCode; await context.Response.WriteAsync(responseBody); } }); } internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) { _downstreamPathOne.ShouldBe(expectedDownstreamPathOne); _downstreamPathTwo.ShouldBe(expectedDownstreamPath); } public void Dispose() { _serviceHandler.Dispose(); _steps.Dispose(); } } public class FakeDepdendency { } public class FakeDefinedAggregator : IDefinedAggregator { private readonly FakeDepdendency _dep; public FakeDefinedAggregator(FakeDepdendency dep) { _dep = dep; } public async Task Aggregate(List responses) { var one = await responses[0].DownstreamResponse.Content.ReadAsStringAsync(); var two = await responses[1].DownstreamResponse.Content.ReadAsStringAsync(); var merge = $"{one}, {two}"; merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); var headers = responses.SelectMany(x => x.DownstreamResponse.Headers).ToList(); return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, "some reason"); } } }