diff --git a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs index 07c558fb0..82d451de6 100644 --- a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs +++ b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs @@ -1,75 +1,70 @@ +using Microsoft.Extensions.Options; using Ocelot.Configuration.File; using Ocelot.Infrastructure; using Ocelot.Logging; using Ocelot.Responses; +using Header = System.Collections.Generic.KeyValuePair; namespace Ocelot.Configuration.Creator { public class HeaderFindAndReplaceCreator : IHeaderFindAndReplaceCreator { + private readonly FileGlobalConfiguration _fileGlobalConfiguration; private readonly IPlaceholders _placeholders; private readonly IOcelotLogger _logger; - public HeaderFindAndReplaceCreator(IPlaceholders placeholders, IOcelotLoggerFactory factory) + public HeaderFindAndReplaceCreator(IOptions fileConfiguration, IPlaceholders placeholders, IOcelotLoggerFactory factory) { _logger = factory.CreateLogger(); + _fileGlobalConfiguration = fileConfiguration.Value.GlobalConfiguration; _placeholders = placeholders; } public HeaderTransformations Create(FileRoute fileRoute) { - var upstream = new List(); - var addHeadersToUpstream = new List(); + var upstreamHeaderTransform = Merge(fileRoute.UpstreamHeaderTransform, _fileGlobalConfiguration.UpstreamHeaderTransform); + var (upstream, addHeadersToUpstream) = ProcessHeaders(upstreamHeaderTransform, nameof(fileRoute.UpstreamHeaderTransform)); - foreach (var input in fileRoute.UpstreamHeaderTransform) - { - if (input.Value.Contains(",")) - { - var hAndr = Map(input); - if (!hAndr.IsError) - { - upstream.Add(hAndr.Data); - } - else - { - _logger.LogWarning(() => $"Unable to add UpstreamHeaderTransform {input.Key}: {input.Value}"); - } - } - else - { - addHeadersToUpstream.Add(new AddHeader(input.Key, input.Value)); - } - } + var downstreamHeaderTransform = Merge(fileRoute.DownstreamHeaderTransform, _fileGlobalConfiguration.DownstreamHeaderTransform); + var (downstream, addHeadersToDownstream) = ProcessHeaders(downstreamHeaderTransform, nameof(fileRoute.DownstreamHeaderTransform)); + + return new HeaderTransformations(upstream, downstream, addHeadersToDownstream, addHeadersToUpstream); + } - var downstream = new List(); - var addHeadersToDownstream = new List(); + private (List StreamHeaders, List AddHeaders) ProcessHeaders(IEnumerable
headerTransform, string propertyName = null) + { + var headerPairs = headerTransform ?? Enumerable.Empty
(); - foreach (var input in fileRoute.DownstreamHeaderTransform) + var streamHeaders = new List(); + var addHeaders = new List(); + + foreach (var input in headerPairs) { - if (input.Value.Contains(",")) + if (input.Value.Contains(HeaderFindAndReplace.Comma)) { var hAndr = Map(input); if (!hAndr.IsError) { - downstream.Add(hAndr.Data); + streamHeaders.Add(hAndr.Data); } else { - _logger.LogWarning(() => $"Unable to add DownstreamHeaderTransform {input.Key}: {input.Value}"); + var name = propertyName ?? "Headers Transformation"; + _logger.LogWarning(() => $"Unable to add {name} {input.Key}: {input.Value}"); } } else { - addHeadersToDownstream.Add(new AddHeader(input.Key, input.Value)); + addHeaders.Add(new AddHeader(input.Key, input.Value)); } } - return new HeaderTransformations(upstream, downstream, addHeadersToDownstream, addHeadersToUpstream); + return (streamHeaders, addHeaders); } - private Response Map(KeyValuePair input) + private Response Map(Header input) { - var findAndReplace = input.Value.Split(','); + var findAndReplace = input.Value.Split(HeaderFindAndReplace.Comma); var replace = findAndReplace[1].TrimStart(); @@ -94,5 +89,18 @@ private Response Map(KeyValuePair input) return new OkResponse(hAndr); } + + /// + /// Merge global Up/Downstream settings to the Route local ones. + /// + /// The Route local settings. + /// Global default settings. + /// An collection. + public static IEnumerable
Merge(Dictionary local, Dictionary global) + { + // Winning strategy: The Route local setting wins over global one + var toAdd = global.ExceptBy(local.Keys, x => x.Key); + return local.Union(toAdd).ToList(); + } } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 7ce35f99e..e24564553 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -13,6 +13,8 @@ public FileGlobalConfiguration() HttpHandlerOptions = new FileHttpHandlerOptions(); CacheOptions = new FileCacheOptions(); MetadataOptions = new FileMetadataOptions(); + UpstreamHeaderTransform = new Dictionary(); + DownstreamHeaderTransform = new Dictionary(); } public string RequestIdKey { get; set; } @@ -48,5 +50,9 @@ public FileGlobalConfiguration() public FileCacheOptions CacheOptions { get; set; } public FileMetadataOptions MetadataOptions { get; set; } + + public Dictionary UpstreamHeaderTransform { get; set; } + + public Dictionary DownstreamHeaderTransform { get; set; } } } diff --git a/src/Ocelot/Configuration/HeaderFindAndReplace.cs b/src/Ocelot/Configuration/HeaderFindAndReplace.cs index 73966cf1d..b9430f522 100644 --- a/src/Ocelot/Configuration/HeaderFindAndReplace.cs +++ b/src/Ocelot/Configuration/HeaderFindAndReplace.cs @@ -1,20 +1,40 @@ -namespace Ocelot.Configuration +namespace Ocelot.Configuration; + +public class HeaderFindAndReplace { - public class HeaderFindAndReplace + public const char Comma = ','; + + public HeaderFindAndReplace(HeaderFindAndReplace from) { - public HeaderFindAndReplace(string key, string find, string replace, int index) - { - Key = key; - Find = find; - Replace = replace; - Index = index; - } + Key = from.Key; + Find = from.Find; + Replace = from.Replace; + Index = from.Index; + } - public string Key { get; } - public string Find { get; } - public string Replace { get; } - - // only index 0 for now.. - public int Index { get; } + public HeaderFindAndReplace(KeyValuePair from) + { + Key = from.Key; + string[] parsed = from.Value.Split(Comma); + Find = parsed[0].Trim(); + Replace = parsed[1].Trim(); + Index = 0; } -} + + public HeaderFindAndReplace(string key, string find, string replace, int index) + { + Key = key; + Find = find; + Replace = replace; + Index = index; + } + + public string Key { get; } + public string Find { get; } + public string Replace { get; } + + // only index 0 for now.. + public int Index { get; } + + public override string ToString() => $"{Key} at {Index}: {Find} → {Replace}"; +} diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs index b5f42d6e3..4fd8e198c 100644 --- a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -1,308 +1,388 @@ +using Microsoft.Extensions.Options; using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Infrastructure; using Ocelot.Logging; using Ocelot.Responses; -using Ocelot.UnitTests.Responder; +using Ocelot.UnitTests.Responder; -namespace Ocelot.UnitTests.Configuration +namespace Ocelot.UnitTests.Configuration; + +public class HeaderFindAndReplaceCreatorTests : UnitTest { - public class HeaderFindAndReplaceCreatorTests : UnitTest + private readonly HeaderFindAndReplaceCreator _creator; + private readonly FileGlobalConfiguration _global; + private FileRoute _route; + private HeaderTransformations _result; + private readonly Mock _placeholders; + private readonly Mock _factory; + private readonly Mock _logger; + + public HeaderFindAndReplaceCreatorTests() + { + _logger = new Mock(); + _factory = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _placeholders = new Mock(); + + _global = new FileGlobalConfiguration(); + _global.UpstreamHeaderTransform.Add("TestGlobal", "Test, Chicken"); + _global.UpstreamHeaderTransform.Add("MoopGlobal", "o, a"); + _global.DownstreamHeaderTransform.Add("PopGlobal", "West, East"); + _global.DownstreamHeaderTransform.Add("BopGlobal", "e, r"); + + var options = new Mock>(); + options.Setup(x => x.Value).Returns(new FileConfiguration + { + GlobalConfiguration = _global, + }); + + _creator = new HeaderFindAndReplaceCreator(options.Object, _placeholders.Object, _factory.Object); + } + + [Fact] + public void Should_create() { - private readonly HeaderFindAndReplaceCreator _creator; - private FileRoute _route; - private HeaderTransformations _result; - private readonly Mock _placeholders; - private readonly Mock _factory; - private readonly Mock _logger; - - public HeaderFindAndReplaceCreatorTests() - { - _logger = new Mock(); - _factory = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _placeholders = new Mock(); - _creator = new HeaderFindAndReplaceCreator(_placeholders.Object, _factory.Object); - } - - [Fact] - public void should_create() - { - var route = new FileRoute + // Arrange + var route = new FileRoute + { + UpstreamHeaderTransform = new Dictionary { - UpstreamHeaderTransform = new Dictionary - { - {"Test", "Test, Chicken"}, - {"Moop", "o, a"}, - }, - DownstreamHeaderTransform = new Dictionary - { - {"Pop", "West, East"}, - {"Bop", "e, r"}, - }, - }; - - var upstream = new List + {"Test", "Test, Chicken"}, + {"Moop", "o, a"}, + }, + DownstreamHeaderTransform = new Dictionary { - new("Test", "Test", "Chicken", 0), - new("Moop", "o", "a", 0), - }; - - var downstream = new List - { - new("Pop", "West", "East", 0), - new("Bop", "e", "r", 0), - }; - - this.Given(x => GivenTheRoute(route)) - .When(x => WhenICreate()) - .Then(x => ThenTheFollowingUpstreamIsReturned(upstream)) - .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) - .BDDfy(); - } - - [Fact] - public void should_create_with_add_headers_to_request() + {"Pop", "West, East"}, + {"Bop", "e, r"}, + }, + }; + var upstream = new List { - const string key = "X-Forwarded-For"; - const string value = "{RemoteIpAddress}"; - - var route = new FileRoute - { - UpstreamHeaderTransform = new Dictionary - { - {key, value}, - }, - }; - - var expected = new AddHeader(key, value); - - this.Given(x => GivenTheRoute(route)) - .When(x => WhenICreate()) - .Then(x => ThenTheFollowingAddHeaderToUpstreamIsReturned(expected)) - .BDDfy(); - } - - [Fact] - public void should_use_base_url_placeholder() + new("Test", "Test", "Chicken", 0), + new("Moop", "o", "a", 0), + new("TestGlobal", "Test", "Chicken", 0), + new("MoopGlobal", "o", "a", 0), + }; + var downstream = new List { - var route = new FileRoute + new("Pop", "West", "East", 0), + new("Bop", "e", "r", 0), + new(_global.DownstreamHeaderTransform.First()), + new(_global.DownstreamHeaderTransform.Last()), + }; + GivenTheRoute(route); + + // Act + WhenICreate(); + + // Assert + ThenTheFollowingUpstreamIsReturned(upstream); + ThenTheFollowingDownstreamIsReturned(downstream); + } + + [Fact] + public void Should_create_with_add_headers_to_request() + { + // Arrange + const string key = "X-Forwarded-For"; + const string value = "{RemoteIpAddress}"; + var route = new FileRoute + { + UpstreamHeaderTransform = new Dictionary { - DownstreamHeaderTransform = new Dictionary - { - {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, - }, - }; + {key, value}, + }, + }; + var expected = new AddHeader(key, value); + GivenTheRoute(route); - var downstream = new List - { - new("Location", "http://www.bbc.co.uk/", "http://ocelot.com/", 0), - }; - - this.Given(x => GivenTheRoute(route)) - .And(x => GivenThePlaceholderIs("http://ocelot.com/")) - .When(x => WhenICreate()) - .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) - .BDDfy(); - } - - [Fact] - public void should_log_errors_and_not_add_headers() - { - var route = new FileRoute - { - DownstreamHeaderTransform = new Dictionary - { - {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, - }, - UpstreamHeaderTransform = new Dictionary - { - {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, - }, - }; - - var expected = new List(); - - this.Given(x => GivenTheRoute(route)) - .And(x => GivenTheBaseUrlErrors()) - .When(x => WhenICreate()) - .Then(x => ThenTheFollowingDownstreamIsReturned(expected)) - .And(x => ThenTheFollowingUpstreamIsReturned(expected)) - .And(x => ThenTheLoggerIsCalledCorrectly("Unable to add DownstreamHeaderTransform Location: http://www.bbc.co.uk/, {BaseUrl}")) - .And(x => ThenTheLoggerIsCalledCorrectly("Unable to add UpstreamHeaderTransform Location: http://www.bbc.co.uk/, {BaseUrl}")) - .BDDfy(); - } - - private void ThenTheLoggerIsCalledCorrectly(string message) - { - _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == message)), Times.Once); - } + // Act + WhenICreate(); - [Fact] - public void should_use_base_url_partial_placeholder() - { - var route = new FileRoute - { - DownstreamHeaderTransform = new Dictionary - { - {"Location", "http://www.bbc.co.uk/pay, {BaseUrl}pay"}, - }, - }; + // Assert + ThenTheFollowingAddHeaderToUpstreamIsReturned(expected); + } - var downstream = new List + [Fact] + public void Should_use_base_url_placeholder() + { + // Arrange + var route = new FileRoute + { + DownstreamHeaderTransform = new Dictionary { - new("Location", "http://www.bbc.co.uk/pay", "http://ocelot.com/pay", 0), - }; - - this.Given(x => GivenTheRoute(route)) - .And(x => GivenThePlaceholderIs("http://ocelot.com/")) - .When(x => WhenICreate()) - .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) - .BDDfy(); - } - - [Fact] - public void should_map_with_partial_placeholder_in_the_middle() + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + }, + }; + var downstream = new List { - var route = new FileRoute - { - DownstreamHeaderTransform = new Dictionary - { - {"Host-Next", "www.bbc.co.uk, subdomain.{Host}/path"}, - }, - }; + new("Location", "http://www.bbc.co.uk/", "http://ocelot.com/", 0), + new(_global.DownstreamHeaderTransform.First()), + new(_global.DownstreamHeaderTransform.Last()), + }; + GivenTheRoute(route); + GivenThePlaceholderIs("http://ocelot.com/"); + + // Act + WhenICreate(); + + // Assert + ThenTheFollowingDownstreamIsReturned(downstream); + } - var expected = new List + [Fact] + [Trait("Feat", "204")] + public void Should_log_errors_and_not_add_headers() + { + // Arrange + var route = new FileRoute + { + DownstreamHeaderTransform = new Dictionary { - new("Host-Next", "www.bbc.co.uk", "subdomain.ocelot.next/path", 0), - }; - - this.Given(x => GivenTheRoute(route)) - .And(x => GivenThePlaceholderIs("ocelot.next")) - .When(x => WhenICreate()) - .Then(x => ThenTheFollowingDownstreamIsReturned(expected)) - .BDDfy(); - } - - [Fact] - public void should_add_trace_id_header() - { - var route = new FileRoute + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + }, + UpstreamHeaderTransform = new Dictionary { - DownstreamHeaderTransform = new Dictionary - { - {"Trace-Id", "{TraceId}"}, - }, - }; - - var expected = new AddHeader("Trace-Id", "{TraceId}"); - - this.Given(x => GivenTheRoute(route)) - .And(x => GivenThePlaceholderIs("http://ocelot.com/")) - .When(x => WhenICreate()) - .Then(x => ThenTheFollowingAddHeaderToDownstreamIsReturned(expected)) - .BDDfy(); - } - - [Fact] - public void should_add_downstream_header_as_is_when_no_replacement_is_given() + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + }, + }; + var expectedDownstream = new List { - var route = new FileRoute - { - DownstreamHeaderTransform = new Dictionary - { - {"X-Custom-Header", "Value"}, - }, - }; - - var expected = new AddHeader("X-Custom-Header", "Value"); - - this.Given(x => GivenTheRoute(route)) - .And(x => WhenICreate()) - .Then(x => x.ThenTheFollowingAddHeaderToDownstreamIsReturned(expected)) - .BDDfy(); - } - - [Fact] - public void should_add_upstream_header_as_is_when_no_replacement_is_given() + new(_global.DownstreamHeaderTransform.First()), + new(_global.DownstreamHeaderTransform.Last()), + }; + var expectedUpstream = new List { - var route = new FileRoute + new("TestGlobal", "Test", "Chicken", 0), + new("MoopGlobal", "o", "a", 0), + }; + GivenTheRoute(route); + GivenTheBaseUrlErrors(); + + // Act + WhenICreate(); + + // Assert + ThenTheFollowingDownstreamIsReturned(expectedDownstream); + ThenTheFollowingUpstreamIsReturned(expectedUpstream); + ThenTheLoggerIsCalledCorrectly($"Unable to add {nameof(FileRoute.DownstreamHeaderTransform)} Location: http://www.bbc.co.uk/, {{BaseUrl}}"); + ThenTheLoggerIsCalledCorrectly($"Unable to add {nameof(FileRoute.UpstreamHeaderTransform)} Location: http://www.bbc.co.uk/, {{BaseUrl}}"); + } + + private void ThenTheLoggerIsCalledCorrectly(string message) => _logger + .Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == message)), + Times.Once); + + [Fact] + public void Should_use_base_url_partial_placeholder() + { + // Arrange + var route = new FileRoute + { + DownstreamHeaderTransform = new Dictionary { - UpstreamHeaderTransform = new Dictionary - { - {"X-Custom-Header", "Value"}, - }, - }; - - var expected = new AddHeader("X-Custom-Header", "Value"); - - this.Given(x => GivenTheRoute(route)) - .And(x => WhenICreate()) - .Then(x => x.ThenTheFollowingAddHeaderToUpstreamIsReturned(expected)) - .BDDfy(); - } - - private void GivenThePlaceholderIs(string placeholderValue) + {"Location", "http://www.bbc.co.uk/pay, {BaseUrl}pay"}, + }, + }; + var downstream = new List { - _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(placeholderValue)); - } - - private void GivenTheBaseUrlErrors() - { - _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); - } + new("Location", "http://www.bbc.co.uk/pay", "http://ocelot.com/pay", 0), + new(_global.DownstreamHeaderTransform.First()), + new(_global.DownstreamHeaderTransform.Last()), + }; + GivenTheRoute(route); + GivenThePlaceholderIs("http://ocelot.com/"); + + // Act + WhenICreate(); + + // Assert + ThenTheFollowingDownstreamIsReturned(downstream); + } - private void ThenTheFollowingAddHeaderToDownstreamIsReturned(AddHeader addHeader) + [Fact] + [Trait("Feat", "204")] + public void Should_map_with_partial_placeholder_in_the_middle() + { + // Arrange + var route = new FileRoute { - _result.AddHeadersToDownstream[0].Key.ShouldBe(addHeader.Key); - _result.AddHeadersToDownstream[0].Value.ShouldBe(addHeader.Value); - } - - private void ThenTheFollowingAddHeaderToUpstreamIsReturned(AddHeader addHeader) + DownstreamHeaderTransform = new Dictionary + { + {"Host-Next", "www.bbc.co.uk, subdomain.{Host}/path"}, + }, + }; + var expected = new List { - _result.AddHeadersToUpstream[0].Key.ShouldBe(addHeader.Key); - _result.AddHeadersToUpstream[0].Value.ShouldBe(addHeader.Value); - } - - private void ThenTheFollowingDownstreamIsReturned(List downstream) + new("Host-Next", "www.bbc.co.uk", "subdomain.ocelot.next/path", 0), + new(_global.DownstreamHeaderTransform.First()), + new(_global.DownstreamHeaderTransform.Last()), + }; + GivenTheRoute(route); + GivenThePlaceholderIs("ocelot.next"); + + // Act + WhenICreate(); + + // Assert + ThenTheFollowingDownstreamIsReturned(expected); + } + + [Fact] + public void Should_add_trace_id_header() + { + // Arrange + var route = new FileRoute { - _result.Downstream.Count.ShouldBe(downstream.Count); + DownstreamHeaderTransform = new Dictionary + { + {"Trace-Id", "{TraceId}"}, + }, + }; + var expected = new AddHeader("Trace-Id", "{TraceId}"); + GivenTheRoute(route); + GivenThePlaceholderIs("http://ocelot.com/"); + + // Act + WhenICreate(); + + // Assert + ThenTheFollowingAddHeaderToDownstreamIsReturned(expected); + } - for (var i = 0; i < _result.Downstream.Count; i++) + [Fact] + public void Should_add_downstream_header_as_is_when_no_replacement_is_given() + { + // Arrange + var route = new FileRoute + { + DownstreamHeaderTransform = new Dictionary { - var result = _result.Downstream[i]; - var expected = downstream[i]; - result.Find.ShouldBe(expected.Find); - result.Index.ShouldBe(expected.Index); - result.Key.ShouldBe(expected.Key); - result.Replace.ShouldBe(expected.Replace); - } - } - - private void GivenTheRoute(FileRoute route) - { - _route = route; - } + {"X-Custom-Header", "Value"}, + }, + }; + var expected = new AddHeader("X-Custom-Header", "Value"); + GivenTheRoute(route); - private void WhenICreate() - { - _result = _creator.Create(_route); - } + // Act + WhenICreate(); - private void ThenTheFollowingUpstreamIsReturned(List expecteds) - { - _result.Upstream.Count.ShouldBe(expecteds.Count); + // Assert + ThenTheFollowingAddHeaderToDownstreamIsReturned(expected); + } - for (var i = 0; i < _result.Upstream.Count; i++) + [Fact] + public void Should_add_upstream_header_as_is_when_no_replacement_is_given() + { + // Arrange + var route = new FileRoute + { + UpstreamHeaderTransform = new Dictionary { - var result = _result.Upstream[i]; - var expected = expecteds[i]; - result.Find.ShouldBe(expected.Find); - result.Index.ShouldBe(expected.Index); - result.Key.ShouldBe(expected.Key); - result.Replace.ShouldBe(expected.Replace); - } - } + {"X-Custom-Header", "Value"}, + }, + }; + var expected = new AddHeader("X-Custom-Header", "Value"); + GivenTheRoute(route); + + // Act + WhenICreate(); + + // Assert + ThenTheFollowingAddHeaderToUpstreamIsReturned(expected); + } + + [Fact] + public void Should_merge() + { + // Arrange + var local = new Dictionary() + { + { "B", "localB" }, + { "C", "localC" }, + }; + var global = new Dictionary() + { + { "A", "globalA" }, + { "B", "globalB" }, + }; + + // Act + var actual = HeaderFindAndReplaceCreator.Merge(local, global); + + // Assert + actual.ShouldNotBeNull(); + var dictionary = actual.ToDictionary(x => x.Key, x => x.Value); + dictionary.Count.ShouldBe(3); + dictionary.ContainsKey("A").ShouldBeTrue(); + dictionary["A"].ShouldBe("globalA"); + dictionary.ContainsKey("B").ShouldBeTrue(); + dictionary["B"].ShouldBe("localB"); // local value wins over global one + dictionary.ContainsKey("C").ShouldBeTrue(); + dictionary["C"].ShouldBe("localC"); + } + + private void GivenThePlaceholderIs(string placeholderValue) + { + _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(placeholderValue)); } + + private void GivenTheBaseUrlErrors() + { + _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); + } + + private void ThenTheFollowingAddHeaderToDownstreamIsReturned(AddHeader addHeader) + { + _result.AddHeadersToDownstream[0].Key.ShouldBe(addHeader.Key); + _result.AddHeadersToDownstream[0].Value.ShouldBe(addHeader.Value); + } + + private void ThenTheFollowingAddHeaderToUpstreamIsReturned(AddHeader addHeader) + { + _result.AddHeadersToUpstream[0].Key.ShouldBe(addHeader.Key); + _result.AddHeadersToUpstream[0].Value.ShouldBe(addHeader.Value); + } + + private void ThenTheFollowingDownstreamIsReturned(List downstream) + { + _result.Downstream.Count.ShouldBe(downstream.Count); + + for (var i = 0; i < _result.Downstream.Count; i++) + { + var result = _result.Downstream[i]; + var expected = downstream[i]; + result.Find.ShouldBe(expected.Find); + result.Index.ShouldBe(expected.Index); + result.Key.ShouldBe(expected.Key); + result.Replace.ShouldBe(expected.Replace); + } + } + + private void GivenTheRoute(FileRoute route) + { + _route = route; + } + + private void WhenICreate() + { + _result = _creator.Create(_route); + } + + private void ThenTheFollowingUpstreamIsReturned(List expecteds) + { + _result.Upstream.Count.ShouldBe(expecteds.Count); + + for (var i = 0; i < _result.Upstream.Count; i++) + { + var result = _result.Upstream[i]; + var expected = expecteds[i]; + result.Find.ShouldBe(expected.Find); + result.Index.ShouldBe(expected.Index); + result.Key.ShouldBe(expected.Key); + result.Replace.ShouldBe(expected.Replace); + } + } }