From b0bdeb94027f080c4ec62bbd7e1c69e5d5611ebb Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sat, 25 Aug 2018 12:32:56 +0100 Subject: [PATCH] #534 fixed failing tests for this issue (#575) --- .../Creator/UpstreamTemplatePatternCreator.cs | 173 +++--- test/Ocelot.AcceptanceTests/RoutingTests.cs | 35 ++ .../UpstreamTemplatePatternCreatorTests.cs | 504 ++++++++-------- .../UrlMatcher/RegExUrlMatcherTests.cs | 560 +++++++++--------- 4 files changed, 676 insertions(+), 596 deletions(-) diff --git a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs index 90ad721dd..fbf62d4ac 100644 --- a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs +++ b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs @@ -1,84 +1,93 @@ -using System.Collections.Generic; -using Ocelot.Configuration.File; -using Ocelot.Values; - -namespace Ocelot.Configuration.Creator -{ - public class UpstreamTemplatePatternCreator : IUpstreamTemplatePatternCreator - { - private const string RegExMatchOneOrMoreOfEverything = ".+"; - private const string RegExMatchEndString = "$"; - private const string RegExIgnoreCase = "(?i)"; - private const string RegExForwardSlashOnly = "^/$"; - private const string RegExForwardSlashAndOnePlaceHolder = "^/.*"; - - public UpstreamPathTemplate Create(IReRoute reRoute) - { +using System.Collections.Generic; +using Ocelot.Configuration.File; +using Ocelot.Values; + +namespace Ocelot.Configuration.Creator +{ + public class UpstreamTemplatePatternCreator : IUpstreamTemplatePatternCreator + { + private const string RegExMatchOneOrMoreOfEverything = ".+"; + private const string RegExMatchOneOrMoreOfEverythingUntilNextForwardSlash = "[^/]+"; + private const string RegExMatchEndString = "$"; + private const string RegExIgnoreCase = "(?i)"; + private const string RegExForwardSlashOnly = "^/$"; + private const string RegExForwardSlashAndOnePlaceHolder = "^/.*"; + + public UpstreamPathTemplate Create(IReRoute reRoute) + { var upstreamTemplate = reRoute.UpstreamPathTemplate; - - - var placeholders = new List(); - - for (var i = 0; i < upstreamTemplate.Length; i++) - { - if (IsPlaceHolder(upstreamTemplate, i)) - { - var postitionOfPlaceHolderClosingBracket = upstreamTemplate.IndexOf('}', i); - var difference = postitionOfPlaceHolderClosingBracket - i + 1; - var placeHolderName = upstreamTemplate.Substring(i, difference); - placeholders.Add(placeHolderName); - - //hack to handle /{url} case - if(ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket)) - { - return new UpstreamPathTemplate(RegExForwardSlashAndOnePlaceHolder, 0, false); - } - } + + var placeholders = new List(); + + for (var i = 0; i < upstreamTemplate.Length; i++) + { + if (IsPlaceHolder(upstreamTemplate, i)) + { + var postitionOfPlaceHolderClosingBracket = upstreamTemplate.IndexOf('}', i); + var difference = postitionOfPlaceHolderClosingBracket - i + 1; + var placeHolderName = upstreamTemplate.Substring(i, difference); + placeholders.Add(placeHolderName); + + //hack to handle /{url} case + if(ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket)) + { + return new UpstreamPathTemplate(RegExForwardSlashAndOnePlaceHolder, 0, false); + } + } } - - var containsQueryString = false; - - if (upstreamTemplate.Contains("?")) - { - containsQueryString = true; - upstreamTemplate = upstreamTemplate.Replace("?", "\\?"); - } - - foreach (var placeholder in placeholders) - { - upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchOneOrMoreOfEverything); - } - - if (upstreamTemplate == "/") - { - return new UpstreamPathTemplate(RegExForwardSlashOnly, reRoute.Priority, containsQueryString); - } - - if(upstreamTemplate.EndsWith("/")) - { - upstreamTemplate = upstreamTemplate.Remove(upstreamTemplate.Length -1, 1) + "(/|)"; - } - - var route = reRoute.ReRouteIsCaseSensitive - ? $"^{upstreamTemplate}{RegExMatchEndString}" - : $"^{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; - - return new UpstreamPathTemplate(route, reRoute.Priority, containsQueryString); - } - - private bool ForwardSlashAndOnePlaceHolder(string upstreamTemplate, List placeholders, int postitionOfPlaceHolderClosingBracket) - { - if(upstreamTemplate.Substring(0, 2) == "/{" && placeholders.Count == 1 && upstreamTemplate.Length == postitionOfPlaceHolderClosingBracket + 1) - { - return true; - } - - return false; - } - - private bool IsPlaceHolder(string upstreamTemplate, int i) - { - return upstreamTemplate[i] == '{'; - } - } -} + + var containsQueryString = false; + + if (upstreamTemplate.Contains("?")) + { + containsQueryString = true; + upstreamTemplate = upstreamTemplate.Replace("?", "\\?"); + } + + for (int i = 0; i < placeholders.Count; i++) + { + var indexOfPlaceholder = upstreamTemplate.IndexOf(placeholders[i]); + var indexOfNextForwardSlash = upstreamTemplate.IndexOf("/", indexOfPlaceholder); + if(indexOfNextForwardSlash < indexOfPlaceholder || (containsQueryString && upstreamTemplate.IndexOf("?") < upstreamTemplate.IndexOf(placeholders[i]))) + { + upstreamTemplate = upstreamTemplate.Replace(placeholders[i], RegExMatchOneOrMoreOfEverything); + } + else + { + upstreamTemplate = upstreamTemplate.Replace(placeholders[i], RegExMatchOneOrMoreOfEverythingUntilNextForwardSlash); + } + } + + if (upstreamTemplate == "/") + { + return new UpstreamPathTemplate(RegExForwardSlashOnly, reRoute.Priority, containsQueryString); + } + + if(upstreamTemplate.EndsWith("/")) + { + upstreamTemplate = upstreamTemplate.Remove(upstreamTemplate.Length -1, 1) + "(/|)"; + } + + var route = reRoute.ReRouteIsCaseSensitive + ? $"^{upstreamTemplate}{RegExMatchEndString}" + : $"^{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; + + return new UpstreamPathTemplate(route, reRoute.Priority, containsQueryString); + } + + private bool ForwardSlashAndOnePlaceHolder(string upstreamTemplate, List placeholders, int postitionOfPlaceHolderClosingBracket) + { + if(upstreamTemplate.Substring(0, 2) == "/{" && placeholders.Count == 1 && upstreamTemplate.Length == postitionOfPlaceHolderClosingBracket + 1) + { + return true; + } + + return false; + } + + private bool IsPlaceHolder(string upstreamTemplate, int i) + { + return upstreamTemplate[i] == '{'; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 19b475757..5fa7bcffd 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -21,6 +21,41 @@ public RoutingTests() _steps = new Steps(); } + [Fact] + public void should_not_match_forward_slash_in_pattern_before_next_forward_slash() + { + var port = 31879; + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/v{apiVersion}/cards", + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/v{apiVersion}/cards", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = port, + } + }, + Priority = 1 + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/api/v1/aaaaaaaaa/cards", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/v1/aaaaaaaaa/cards")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + [Fact] public void should_return_response_404_when_no_configuration_at_all() { diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs index d924586d3..1aec3ec87 100644 --- a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs @@ -1,244 +1,260 @@ -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Ocelot.Values; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - public class UpstreamTemplatePatternCreatorTests - { - private FileReRoute _fileReRoute; - private readonly UpstreamTemplatePatternCreator _creator; - private UpstreamPathTemplate _result; - - public UpstreamTemplatePatternCreatorTests() - { - _creator = new UpstreamTemplatePatternCreator(); - } - - [Fact] - public void should_use_re_route_priority() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/orders/{catchAll}", - Priority = 0 - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/orders/.+$")) - .And(x => ThenThePriorityIs(0)) - .BDDfy(); - } - - [Fact] - public void should_use_zero_priority() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/{catchAll}", - Priority = 1 - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/.*")) - .And(x => ThenThePriorityIs(0)) - .BDDfy(); - } - - [Fact] - public void should_set_upstream_template_pattern_to_ignore_case_sensitivity() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/PRODUCTS/{productId}", - ReRouteIsCaseSensitive = false - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS/.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_match_forward_slash_or_no_forward_slash_if_template_end_with_forward_slash() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/PRODUCTS/", - ReRouteIsCaseSensitive = false - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS(/|)$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_set_upstream_template_pattern_to_respect_case_sensitivity() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/PRODUCTS/{productId}", - ReRouteIsCaseSensitive = true - }; - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/PRODUCTS/.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_anything_to_end_of_string() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/products/{productId}", - ReRouteIsCaseSensitive = true - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/api/products/.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_more_than_one_placeholder() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}", - ReRouteIsCaseSensitive = true - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/api/products/.+/variants/.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_more_than_one_placeholder_with_trailing_slash() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}/", - ReRouteIsCaseSensitive = true - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/api/products/.+/variants/.+(/|)$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_to_end_of_string() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/" - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_to_end_of_string_when_slash_and_placeholder() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/{url}" - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/.*")) - .And(x => ThenThePriorityIs(0)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_starts_with_placeholder_then_has_another_later() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/{productId}/products/variants/{variantId}/", - ReRouteIsCaseSensitive = true - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/.+/products/variants/.+(/|)$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_query_string() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}" - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/subscriptions/.+/updates\\?unitId=.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_query_string_with_multiple_params() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId={productId}" - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/subscriptions/.+/updates\\?unitId=.+&productId=.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - private void GivenTheFollowingFileReRoute(FileReRoute fileReRoute) - { - _fileReRoute = fileReRoute; - } - - private void WhenICreateTheTemplatePattern() - { - _result = _creator.Create(_fileReRoute); - } - - private void ThenTheFollowingIsReturned(string expected) - { - _result.Template.ShouldBe(expected); - } - - private void ThenThePriorityIs(int v) - { - _result.Priority.ShouldBe(v); - } - } -} +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class UpstreamTemplatePatternCreatorTests + { + private FileReRoute _fileReRoute; + private readonly UpstreamTemplatePatternCreator _creator; + private UpstreamPathTemplate _result; + + public UpstreamTemplatePatternCreatorTests() + { + _creator = new UpstreamTemplatePatternCreator(); + } + + [Fact] + public void should_match_up_to_next_slash() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/api/v{apiVersion}/cards", + Priority = 0 + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/v[^/]+/cards$")) + .And(x => ThenThePriorityIs(0)) + .BDDfy(); + } + + [Fact] + public void should_use_re_route_priority() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/orders/{catchAll}", + Priority = 0 + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/orders/.+$")) + .And(x => ThenThePriorityIs(0)) + .BDDfy(); + } + + [Fact] + public void should_use_zero_priority() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/{catchAll}", + Priority = 1 + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/.*")) + .And(x => ThenThePriorityIs(0)) + .BDDfy(); + } + + [Fact] + public void should_set_upstream_template_pattern_to_ignore_case_sensitivity() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/PRODUCTS/{productId}", + ReRouteIsCaseSensitive = false + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS/.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_match_forward_slash_or_no_forward_slash_if_template_end_with_forward_slash() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/PRODUCTS/", + ReRouteIsCaseSensitive = false + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS(/|)$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_set_upstream_template_pattern_to_respect_case_sensitivity() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/PRODUCTS/{productId}", + ReRouteIsCaseSensitive = true + }; + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/PRODUCTS/.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_anything_to_end_of_string() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/api/products/{productId}", + ReRouteIsCaseSensitive = true + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/api/products/.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_more_than_one_placeholder() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}", + ReRouteIsCaseSensitive = true + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[^/]+/variants/.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_more_than_one_placeholder_with_trailing_slash() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}/", + ReRouteIsCaseSensitive = true + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[^/]+/variants/[^/]+(/|)$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_to_end_of_string() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/" + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_to_end_of_string_when_slash_and_placeholder() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/{url}" + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/.*")) + .And(x => ThenThePriorityIs(0)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_starts_with_placeholder_then_has_another_later() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/{productId}/products/variants/{variantId}/", + ReRouteIsCaseSensitive = true + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/[^/]+/products/variants/[^/]+(/|)$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_query_string() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}" + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/subscriptions/[^/]+/updates\\?unitId=.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_query_string_with_multiple_params() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId={productId}" + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/subscriptions/[^/]+/updates\\?unitId=.+&productId=.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + private void GivenTheFollowingFileReRoute(FileReRoute fileReRoute) + { + _fileReRoute = fileReRoute; + } + + private void WhenICreateTheTemplatePattern() + { + _result = _creator.Create(_fileReRoute); + } + + private void ThenTheFollowingIsReturned(string expected) + { + _result.Template.ShouldBe(expected); + } + + private void ThenThePriorityIs(int v) + { + _result.Priority.ShouldBe(v); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs index 10fe106e7..36f2f852c 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -1,271 +1,291 @@ -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Responses; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher -{ - public class RegExUrlMatcherTests - { - private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; - private string _path; - private string _downstreamPathTemplate; - private Response _result; - private string _queryString; - private bool _containsQueryString; - - public RegExUrlMatcherTests() - { - _urlMatcher = new RegExUrlMatcher(); - } - - [Fact] - public void should_match_path_with_no_query_string() - { - const string regExForwardSlashAndOnePlaceHolder = "^(?i)/newThing$"; - - this.Given(x => x.GivenIHaveAUpstreamPath("/newThing")) - .And(_ => GivenIHaveAQueryString("?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern(regExForwardSlashAndOnePlaceHolder)) - .When(x => x.WhenIMatchThePaths()) - .And(x => x.ThenTheResultIsTrue()) - .BDDfy(); +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher +{ + public class RegExUrlMatcherTests + { + private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; + private string _path; + private string _downstreamPathTemplate; + private Response _result; + private string _queryString; + private bool _containsQueryString; + + public RegExUrlMatcherTests() + { + _urlMatcher = new RegExUrlMatcher(); } - - [Fact] - public void should_match_query_string() - { - const string regExForwardSlashAndOnePlaceHolder = "^(?i)/api/subscriptions/.+/updates\\?unitId=.+$"; - - this.Given(x => x.GivenIHaveAUpstreamPath("/api/subscriptions/1/updates")) - .And(_ => GivenIHaveAQueryString("?unitId=2")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern(regExForwardSlashAndOnePlaceHolder)) - .And(_ => GivenThereIsAQueryInTemplate()) - .When(x => x.WhenIMatchThePaths()) - .And(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void should_match_query_string_with_multiple_params() - { - const string regExForwardSlashAndOnePlaceHolder = "^(?i)/api/subscriptions/.+/updates\\?unitId=.+&productId=.+$"; - - this.Given(x => x.GivenIHaveAUpstreamPath("/api/subscriptions/1/updates?unitId=2")) - .And(_ => GivenIHaveAQueryString("?unitId=2&productId=2")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern(regExForwardSlashAndOnePlaceHolder)) - .And(_ => GivenThereIsAQueryInTemplate()) - .When(x => x.WhenIMatchThePaths()) - .And(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void should_not_match_slash_becaue_we_need_to_match_something_after_it() - { - const string regExForwardSlashAndOnePlaceHolder = "^/[0-9a-zA-Z].+"; - - this.Given(x => x.GivenIHaveAUpstreamPath("/")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern(regExForwardSlashAndOnePlaceHolder)) - .When(x => x.WhenIMatchThePaths()) - .And(x => x.ThenTheResultIsFalse()) - .BDDfy(); - } - - [Fact] - public void should_not_match_forward_slash_only_regex() - { - this.Given(x => x.GivenIHaveAUpstreamPath("/working/")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/$")) - .When(x => x.WhenIMatchThePaths()) - .And(x => x.ThenTheResultIsFalse()) - .BDDfy(); - } - - [Fact] - public void should_not_match_issue_134() - { - this.Given(x => x.GivenIHaveAUpstreamPath("/api/vacancy/1/")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^(?i)/vacancy/.+/$")) - .When(x => x.WhenIMatchThePaths()) - .And(x => x.ThenTheResultIsFalse()) - .BDDfy(); - } - - [Fact] - public void should_match_forward_slash_only_regex() - { - this.Given(x => x.GivenIHaveAUpstreamPath("/")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/$")) - .When(x => x.WhenIMatchThePaths()) - .And(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void should_find_match_when_template_smaller_than_valid_path() - { - this.Given(x => x.GivenIHaveAUpstreamPath("/api/products/2354325435624623464235")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/api/products/.+$")) - .When(x => x.WhenIMatchThePaths()) - .And(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void should_not_find_match() - { - this.Given(x => x.GivenIHaveAUpstreamPath("/api/values")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/$")) - .When(x => x.WhenIMatchThePaths()) - .And(x => x.ThenTheResultIsFalse()) - .BDDfy(); - } - - [Fact] - public void can_match_down_stream_url() - { - this.Given(x => x.GivenIHaveAUpstreamPath("")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^$")) - .When(x => x.WhenIMatchThePaths()) - .And(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void can_match_down_stream_url_with_no_slash() - { - this.Given(x => x.GivenIHaveAUpstreamPath("api")) - .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api$")) - .When(x => x.WhenIMatchThePaths()) - .Then(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void can_match_down_stream_url_with_one_slash() - { - this.Given(x => x.GivenIHaveAUpstreamPath("api/")) - .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/$")) - .When(x => x.WhenIMatchThePaths()) - .Then(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void can_match_down_stream_url_with_downstream_template() - { - this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/")) - .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/$")) - .When(x => x.WhenIMatchThePaths()) - .Then(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void can_match_down_stream_url_with_downstream_template_with_one_place_holder() - { - this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1")) - .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/.+$")) - .When(x => x.WhenIMatchThePaths()) - .Then(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void can_match_down_stream_url_with_downstream_template_with_two_place_holders() - { - this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/2")) - .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/.+/.+$")) - .When(x => x.WhenIMatchThePaths()) - .Then(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void can_match_down_stream_url_with_downstream_template_with_two_place_holders_seperated_by_something() - { - this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/.+/categories/.+$")) - .When(x => x.WhenIMatchThePaths()) - .Then(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void can_match_down_stream_url_with_downstream_template_with_three_place_holders_seperated_by_something() - { - this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/123")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/.+/categories/.+/variant/.+$")) - .When(x => x.WhenIMatchThePaths()) - .Then(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void can_match_down_stream_url_with_downstream_template_with_three_place_holders() - { - this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/.+/categories/.+/variant/$")) - .When(x => x.WhenIMatchThePaths()) - .Then(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void should_ignore_case_sensitivity() - { - this.Given(x => x.GivenIHaveAUpstreamPath("API/product/products/1/categories/2/variant/")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^(?i)api/product/products/.+/categories/.+/variant/$")) - .When(x => x.WhenIMatchThePaths()) - .Then(x => x.ThenTheResultIsTrue()) - .BDDfy(); - } - - [Fact] - public void should_respect_case_sensitivity() - { - this.Given(x => x.GivenIHaveAUpstreamPath("API/product/products/1/categories/2/variant/")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/.+/categories/.+/variant/$")) - .When(x => x.WhenIMatchThePaths()) - .Then(x => x.ThenTheResultIsFalse()) - .BDDfy(); - } - - private void GivenIHaveAUpstreamPath(string path) - { - _path = path; - } - - private void GivenIHaveAQueryString(string queryString) - { - _queryString = queryString; - } - - private void GivenIHaveAnUpstreamUrlTemplatePattern(string downstreamUrlTemplate) - { - _downstreamPathTemplate = downstreamUrlTemplate; - } - - private void WhenIMatchThePaths() - { - _result = _urlMatcher.Match(_path, _queryString, _downstreamPathTemplate, _containsQueryString); - } - - private void ThenTheResultIsTrue() - { - _result.Data.Match.ShouldBeTrue(); - } - - private void ThenTheResultIsFalse() - { - _result.Data.Match.ShouldBeFalse(); - } - - private void GivenThereIsAQueryInTemplate() - { - _containsQueryString = true; - } - } -} + + [Fact] + public void should_not_match() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/api/v1/aaaaaaaaa/cards")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^(?i)/api/v[^/]+/cards$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + [Fact] + public void should_match() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/api/v1/cards")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^(?i)/api/v[^/]+/cards$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_match_path_with_no_query_string() + { + const string regExForwardSlashAndOnePlaceHolder = "^(?i)/newThing$"; + + this.Given(x => x.GivenIHaveAUpstreamPath("/newThing")) + .And(_ => GivenIHaveAQueryString("?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern(regExForwardSlashAndOnePlaceHolder)) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_match_query_string() + { + const string regExForwardSlashAndOnePlaceHolder = "^(?i)/api/subscriptions/[^/]+/updates\\?unitId=.+$"; + + this.Given(x => x.GivenIHaveAUpstreamPath("/api/subscriptions/1/updates")) + .And(_ => GivenIHaveAQueryString("?unitId=2")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern(regExForwardSlashAndOnePlaceHolder)) + .And(_ => GivenThereIsAQueryInTemplate()) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_match_query_string_with_multiple_params() + { + const string regExForwardSlashAndOnePlaceHolder = "^(?i)/api/subscriptions/[^/]+/updates\\?unitId=.+&productId=.+$"; + + this.Given(x => x.GivenIHaveAUpstreamPath("/api/subscriptions/1/updates?unitId=2")) + .And(_ => GivenIHaveAQueryString("?unitId=2&productId=2")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern(regExForwardSlashAndOnePlaceHolder)) + .And(_ => GivenThereIsAQueryInTemplate()) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_not_match_slash_becaue_we_need_to_match_something_after_it() + { + const string regExForwardSlashAndOnePlaceHolder = "^/[0-9a-zA-Z].+"; + + this.Given(x => x.GivenIHaveAUpstreamPath("/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern(regExForwardSlashAndOnePlaceHolder)) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + [Fact] + public void should_not_match_forward_slash_only_regex() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/working/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + [Fact] + public void should_not_match_issue_134() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/api/vacancy/1/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^(?i)/vacancy/[^/]+/$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + [Fact] + public void should_match_forward_slash_only_regex() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_find_match_when_template_smaller_than_valid_path() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/api/products/2354325435624623464235")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/api/products/.+$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_not_find_match() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/api/values")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url() + { + this.Given(x => x.GivenIHaveAUpstreamPath("")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_no_slash() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_one_slash() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_one_place_holder() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/.+$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_two_place_holders() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/2")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/[^/]+/.+$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_two_place_holders_seperated_by_something() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/[^/]+/categories/.+$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_three_place_holders_seperated_by_something() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/123")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/[^/]+/categories/[^/]+/variant/.+$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_three_place_holders() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/[^/]+/categories/[^/]+/variant/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_ignore_case_sensitivity() + { + this.Given(x => x.GivenIHaveAUpstreamPath("API/product/products/1/categories/2/variant/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^(?i)api/product/products/[^/]+/categories/[^/]+/variant/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_respect_case_sensitivity() + { + this.Given(x => x.GivenIHaveAUpstreamPath("API/product/products/1/categories/2/variant/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^api/product/products/[^/]+/categories/[^/]+/variant/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + private void GivenIHaveAUpstreamPath(string path) + { + _path = path; + } + + private void GivenIHaveAQueryString(string queryString) + { + _queryString = queryString; + } + + private void GivenIHaveAnUpstreamUrlTemplatePattern(string downstreamUrlTemplate) + { + _downstreamPathTemplate = downstreamUrlTemplate; + } + + private void WhenIMatchThePaths() + { + _result = _urlMatcher.Match(_path, _queryString, _downstreamPathTemplate, _containsQueryString); + } + + private void ThenTheResultIsTrue() + { + _result.Data.Match.ShouldBeTrue(); + } + + private void ThenTheResultIsFalse() + { + _result.Data.Match.ShouldBeFalse(); + } + + private void GivenThereIsAQueryInTemplate() + { + _containsQueryString = true; + } + } +}