Skip to content
This repository was archived by the owner on Jan 24, 2021. It is now read-only.

Commit ac772b5

Browse files
PhilipSkinnerPhilip Skinner
authored and
Philip Skinner
committed
Fixes for strange routing behaviour with optionals.
The routing engine incorrectly routes to longer urls in preference of the score, plus it falls back on routing to the last added matching routes instead of the first. I've fixed the sorting comparison method and added some test coverage to show the particular use cases where I found issues.
1 parent 3943452 commit ac772b5

File tree

2 files changed

+96
-13
lines changed

2 files changed

+96
-13
lines changed

src/Nancy/Routing/Trie/MatchResult.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,37 +65,40 @@ public MatchResult()
6565
/// <param name="other">An object to compare with this object.</param>
6666
public int CompareTo(MatchResult other)
6767
{
68-
// Length of the route takes precedence over score
69-
if (this.RouteLength < other.RouteLength)
68+
// Score of the route takes precedence over length
69+
if (this.Score < other.Score)
7070
{
7171
return -1;
7272
}
7373

74-
if (this.RouteLength > other.RouteLength)
74+
if (this.Score > other.Score)
7575
{
7676
return 1;
7777
}
7878

79-
if (this.Score < other.Score)
79+
// If the score is the same, take the shortest route that matches
80+
if (this.RouteLength < other.RouteLength)
8081
{
81-
return -1;
82+
return 1;
8283
}
8384

84-
if (this.Score > other.Score)
85+
if (this.RouteLength > other.RouteLength)
8586
{
86-
return 1;
87+
return -1;
8788
}
8889

90+
8991
if (Equals(this.ModuleType, other.ModuleType))
9092
{
93+
//Otherwise, we need to take the route that was indexed first
9194
if (this.RouteIndex < other.RouteIndex)
9295
{
93-
return -1;
96+
return 1;
9497
}
9598

9699
if (this.RouteIndex > other.RouteIndex)
97100
{
98-
return 1;
101+
return -1;
99102
}
100103
}
101104

test/Nancy.Tests/Unit/Routing/DefaultRouteResolverFixture.cs

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,11 @@ public async Task Should_resolve_optional_capture_with_default_with_optional_spe
152152
//Then
153153
if (ShouldBeFound(path, caseSensitive))
154154
{
155-
result.Body.AsString().ShouldEqual("OptionalCaptureWithDefault " + expected);
155+
result.Body.AsString().ShouldEqual("OptionalCaptureWithDefault " + expected);
156156
}
157157
else
158158
{
159-
result.StatusCode.ShouldEqual(HttpStatusCode.NotFound);
159+
result.StatusCode.ShouldEqual(HttpStatusCode.NotFound);
160160
}
161161
}
162162

@@ -174,14 +174,73 @@ public async Task Should_resolve_optional_capture_with_default_with_optional_not
174174
//Then
175175
if (ShouldBeFound(path, caseSensitive))
176176
{
177-
result.Body.AsString().ShouldEqual("OptionalCaptureWithDefault test");
177+
result.Body.AsString().ShouldEqual("OptionalCaptureWithDefault test");
178178
}
179179
else
180180
{
181-
result.StatusCode.ShouldEqual(HttpStatusCode.NotFound);
181+
result.StatusCode.ShouldEqual(HttpStatusCode.NotFound);
182182
}
183183
}
184184

185+
[Theory]
186+
[InlineData("/api", "Single optional segment")]
187+
[InlineData("/api/", "Single optional segment")]
188+
[InlineData("/api/arg1", "Single optional segment")]
189+
[InlineData("/api/arg1/", "Single optional segment")]
190+
[InlineData("/api/arg1/arg2", "Two optional segments")]
191+
[InlineData("/api/arg1/arg2/", "Two optional segments")]
192+
[InlineData("/api/arg1/arg2/arg3", "Three optional segments")]
193+
[InlineData("/api/arg1/arg2/arg3/", "Three optional segments")]
194+
public async Task Should_Resolve_Optionals_Correctly(string path, string expected)
195+
{
196+
var browser = InitBrowser(caseSensitive: false);
197+
var result = await browser.Get(path);
198+
result.Body.AsString().ShouldEqual(expected);
199+
}
200+
201+
[Theory]
202+
[InlineData("/api/greedy/arg1", "Greedy match")]
203+
[InlineData("/api/greedy/arg1/arg2", "Greedy match")]
204+
public async Task Should_Resolve_Greedy_Alongside_Optionals(string path, string expected)
205+
{
206+
var browser = InitBrowser(caseSensitive: false);
207+
var result = await browser.Get(path);
208+
result.Body.AsString().ShouldEqual(expected);
209+
}
210+
211+
[Theory]
212+
[InlineData("/optional/literal/bar", "Single optional segment, literal segment at end")]
213+
[InlineData("/optional/literal/arg1/bar", "Single optional segment, literal segment at end")]
214+
[InlineData("/optional/literal/arg1/arg2/bar", "Two optional segments, literal segment at end")]
215+
public async Task Should_Resolve_Optionals_with_Literal_Ends(string path, string expected)
216+
{
217+
var browser = InitBrowser(caseSensitive: false);
218+
var result = await browser.Get(path);
219+
result.Body.AsString().ShouldEqual(expected);
220+
}
221+
222+
[Theory]
223+
[InlineData("/optional/variable/hello", "Single hello")]
224+
[InlineData("/optional/variable/hello/there", "Single hello there")]
225+
[InlineData("/optional/variable/hello/there/everybody", "Double hello there everybody")]
226+
public async Task Should_Resolve_Optionals_with_Variable_Ends(string path, string expected)
227+
{
228+
var browser = InitBrowser(caseSensitive: false);
229+
var result = await browser.Get(path);
230+
result.Body.AsString().ShouldEqual(expected);
231+
}
232+
233+
[Theory]
234+
[InlineData("/too/greedy/arg1", "One arg1")]
235+
[InlineData("/too/greedy/arg1/hello", "Literal arg1")]
236+
public async Task Should_Not_Be_Too_Greedy(string path, string expected)
237+
{
238+
var browser = InitBrowser(caseSensitive: false);
239+
var result = await browser.Get(path);
240+
var woot = result.Body.AsString();
241+
result.Body.AsString().ShouldEqual(expected);
242+
}
243+
185244
[Theory]
186245
[InlineData("/bleh/this/is/some/stuff", true, "this/is/some/stuff")]
187246
[InlineData("/bleh/this/is/some/stuff", false, "this/is/some/stuff")]
@@ -442,6 +501,7 @@ public NoRootModule()
442501
}
443502
}
444503

504+
445505
private class TestModule : NancyModule
446506
{
447507
public TestModule()
@@ -475,6 +535,26 @@ public TestModule()
475535
Get("/capturenodewithliteral/{file}.html", args => "CaptureNodeWithLiteral " + args.file + ".html");
476536

477537
Get(@"/regex/(?<foo>\d{2,4})/{bar}", args => string.Format("RegEx {0} {1}", args.foo, args.bar));
538+
539+
Get("/api/{arg1?}", args => "Single optional segment");
540+
541+
Get("/api/{arg1?}/{arg2?}", args => "Two optional segments");
542+
543+
Get("/api/{arg1?}/{arg2?}/{arg3?}", args => "Three optional segments");
544+
545+
Get("/api/greedy/{something*}", args => "Greedy match");
546+
547+
Get("/optional/literal/{arg1?}/bar", args => "Single optional segment, literal segment at end");
548+
549+
Get("/optional/literal/{arg1?}/{arg2?}/bar", args => "Two optional segments, literal segment at end");
550+
551+
Get("/optional/variable/{arg1?}/{variable}", args => string.Format("Single {0} {1}", args.arg1.Value, args.variable.Value));
552+
553+
Get("/optional/variable/{arg1?}/{arg2?}/{variable}", args => string.Format("Double {0} {1} {2}", args.arg1.Value, args.arg2.Value, args.variable.Value));
554+
555+
Get("/too/greedy/{arg1*}", args => string.Format("One {0}", args.arg1.Value));
556+
557+
Get("/too/greedy/{arg1*}/hello", args => string.Format("Literal {0}", args.arg1.Value));
478558
}
479559
}
480560
}

0 commit comments

Comments
 (0)