diff --git a/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs b/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs new file mode 100644 index 0000000000..f2a3671f82 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing.Template +{ + public class BoundRouteTemplate + { + public string Path { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/Template.cs b/src/Microsoft.AspNet.Routing/Template/Template.cs new file mode 100644 index 0000000000..13fb861ded --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/Template.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNet.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class Template + { + private const string SeparatorString = "/"; + + private readonly TemplateMatcher _matcher; + private readonly TemplateBinder _binder; + + public Template(List segments) + { + if (segments == null) + { + throw new ArgumentNullException("segments"); + } + + Segments = segments; + + Parameters = new List(); + for (var i = 0; i < segments.Count; i++) + { + var segment = Segments[i]; + for (var j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + if (part.IsParameter) + { + Parameters.Add(part); + } + } + } + + _matcher = new TemplateMatcher(this); + _binder = new TemplateBinder(this); + } + + public List Parameters { get; private set; } + + public List Segments { get; private set; } + + public IDictionary Match(string requestPath, IDictionary defaults) + { + return _matcher.Match(requestPath, defaults); + } + + private string DebuggerToString() + { + return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString())); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs new file mode 100644 index 0000000000..638af9dd8e --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs @@ -0,0 +1,580 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.AspNet.Routing.Template +{ + public class TemplateBinder + { + public TemplateBinder(Template template) + { + if (template == null) + { + throw new ArgumentNullException("template"); + } + + Template = template; + } + + public Template Template { get; private set; } + + public BoundRouteTemplate Bind(IDictionary defaults, IDictionary ambientValues, IDictionary values) + { + if (values == null) + { + throw new ArgumentNullException("values"); + } + + var context = GetAcceptedValues(defaults, ambientValues, values); + if (context == null) + { + // We couldn't get values for all the required parameters + return null; + } + + return BindValues(context); + } + + // Step 1: Get the list of values we're going to try to use to match and generate this URI + private TemplateBindingContext GetAcceptedValues(IDictionary defaults, IDictionary ambientValues, IDictionary values) + { + Contract.Assert(values != null); + + var context = new TemplateBindingContext(defaults, values); + + // Find out which entries in the URI are valid for the URI we want to generate. + // If the URI had ordered parameters a="1", b="2", c="3" and the new values + // specified that b="9", then we need to invalidate everything after it. The new + // values should then be a="1", b="9", c=. + for (var i = 0; i < Template.Parameters.Count; i++) + { + var parameter = Template.Parameters[i]; + + // If it's a parameter subsegment, examine the current value to see if it matches the new value + var parameterName = parameter.Name; + + object newParameterValue; + var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); + if (hasNewParameterValue) + { + context.Use(parameterName); + } + + object currentParameterValue = null; + var hasCurrentParameterValue = ambientValues != null && ambientValues.TryGetValue(parameterName, out currentParameterValue); + + if (hasNewParameterValue && hasCurrentParameterValue) + { + if (!RoutePartsEqual(currentParameterValue, newParameterValue)) + { + // Stop copying current values when we find one that doesn't match + break; + } + } + + // If the parameter is a match, add it to the list of values we will use for URI generation + if (hasNewParameterValue) + { + if (IsRoutePartNonEmpty(newParameterValue)) + { + context.Accept(parameterName, newParameterValue); + } + } + else + { + if (hasCurrentParameterValue) + { + context.Accept(parameterName, currentParameterValue); + } + } + }; + + // Add all remaining new values to the list of values we will use for URI generation + foreach (var kvp in values) + { + if (IsRoutePartNonEmpty(kvp.Value)) + { + context.Accept(kvp.Key, kvp.Value); + } + } + + // Add all current values that aren't in the URI at all + if (ambientValues != null) + { + foreach (var kvp in ambientValues) + { + var parameter = GetParameter(kvp.Key); + if (parameter == null) + { + context.Accept(kvp.Key, kvp.Value); + } + } + } + + // Accept all remaining default values if they match a required parameter + for (int i = 0; i < Template.Parameters.Count; i++) + { + var parameter = Template.Parameters[i]; + if (parameter.IsOptional || parameter.IsCatchAll) + { + continue; + } + + if (context.NeedsValue(parameter.Name)) + { + // Add the default value only if there isn't already a new value for it and + // only if it actually has a default value, which we determine based on whether + // the parameter value is required. + context.AcceptDefault(parameter.Name); + } + } + + // Validate that all required parameters have a value. + for (var i = 0; i < Template.Parameters.Count; i++) + { + var parameter = Template.Parameters[i]; + if (parameter.IsOptional || parameter.IsCatchAll) + { + continue; + } + + if (!context.AcceptedValues.ContainsKey(parameter.Name)) + { + // We don't have a value for this parameter, so we can't generate a url. + return null; + } + } + + // Any default values that don't appear as parameters are treated like filters. Any new values + // provided must match these defaults. + if (context.Filters != null) + { + foreach (var filter in context.Filters) + { + var parameter = GetParameter(filter.Key); + if (parameter != null) + { + continue; + } + + object value; + if (values.TryGetValue(filter.Key, out value)) + { + if (RoutePartsEqual(value, filter.Value)) + { + context.Use(filter.Key); + } + else + { + // If there is a non-parameterized value in the route and there is a + // new value for it and it doesn't match, this route won't match. + return null; + } + } + } + } + + return context; + } + + // Step 2: If the route is a match generate the appropriate URI + private BoundRouteTemplate BindValues(TemplateBindingContext bindingContext) + { + var context = new UriBuildingContext(); + + for (var i = 0; i < Template.Segments.Count; i++) + { + Contract.Assert(context.BufferState == SegmentState.Beginning); + Contract.Assert(context.UriState == SegmentState.Beginning); + + var segment = Template.Segments[i]; + + for (var j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + + if (part.IsLiteral) + { + if (!context.Accept(part.Text)) + { + return null; + } + } + else if (part.IsParameter) + { + // If it's a parameter, get its value + object value; + var hasValue = bindingContext.AcceptedValues.TryGetValue(part.Name, out value); + if (hasValue) + { + bindingContext.Use(part.Name); + } + + var converted = Convert.ToString(value, CultureInfo.InvariantCulture); + if (bindingContext.AcceptedDefaultValues.Contains(part.Name)) + { + // If the accepted value is the same as the default value buffer it since + // we won't necessarily add it to the URI we generate. + if (!context.Buffer(converted)) + { + return null; + } + } + else + { + if (!context.Accept(converted)) + { + return null; + } + } + } + } + + context.EndSegment(); + } + + // Encode the URI before we append the query string, otherwise we would double encode the query string + var encoded = new StringBuilder(); + encoded.Append(UriEncode(context.Build())); + + // Generate the query string + var firstParam = true; + foreach (var kvp in bindingContext.UnusedValues) + { + var converted = Convert.ToString(kvp.Value, CultureInfo.InvariantCulture); + if (String.IsNullOrEmpty(converted)) + { + continue; + } + + encoded.Append(firstParam ? '?' : '&'); + firstParam = false; + + encoded.Append(Uri.EscapeDataString(kvp.Key)); + encoded.Append('='); + encoded.Append(Uri.EscapeDataString(converted)); + } + + return new BoundRouteTemplate() + { + Path = encoded.ToString(), + }; + } + + private static string UriEncode(string str) + { + string escape = Uri.EscapeUriString(str); + return Regex.Replace(escape, "([#;?:@&=+$,])", EscapeReservedCharacters); + } + + private static string EscapeReservedCharacters(Match m) + { + return "%" + Convert.ToUInt16(m.Value[0]).ToString("x2", CultureInfo.InvariantCulture); + } + + private TemplatePart GetParameter(string name) + { + for (int i = 0; i < Template.Parameters.Count; i++) + { + var parameter = Template.Parameters[i]; + if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase)) + { + return parameter; + } + } + + return null; + } + + private static bool RoutePartsEqual(object a, object b) + { + string sa = a as string; + string sb = b as string; + + if (sa != null && sb != null) + { + // For strings do a case-insensitive comparison + return string.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); + } + else + { + if (a != null && b != null) + { + // Explicitly call .Equals() in case it is overridden in the type + return a.Equals(b); + } + else + { + // At least one of them is null. Return true if they both are + return a == b; + } + } + } + + private static bool IsRoutePartNonEmpty(object routePart) + { + var routePartString = routePart as string; + if (routePartString == null) + { + return routePart != null; + } + else + { + return routePartString.Length > 0; + } + } + + [DebuggerDisplay("{DebuggerToString(),nq}")] + private class TemplateBindingContext + { + private readonly IDictionary _defaults; + + private readonly Dictionary _acceptedValues; + private readonly HashSet _acceptedDefaultValues; + private readonly Dictionary _unusedValues; + private readonly Dictionary _filters; + + public TemplateBindingContext(IDictionary defaults, IDictionary values) + { + if (values == null) + { + throw new ArgumentNullException("values"); + } + + _defaults = defaults; + + _acceptedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + _acceptedDefaultValues = new HashSet(StringComparer.OrdinalIgnoreCase); + _unusedValues = new Dictionary(values, StringComparer.OrdinalIgnoreCase); + + if (_defaults != null) + { + _filters = new Dictionary(defaults, StringComparer.OrdinalIgnoreCase); + } + } + + public Dictionary AcceptedValues + { + get { return _acceptedValues; } + } + + /// + /// These are values that are equivalent to the default. These aren't written to the url unless + /// necessary. + /// > + public HashSet AcceptedDefaultValues + { + get { return _acceptedDefaultValues; } + } + + public Dictionary UnusedValues + { + get { return _unusedValues; } + } + + public Dictionary Filters + { + get { return _filters; } + } + + public void Accept(string key, object value) + { + if (!_acceptedValues.ContainsKey(key)) + { + _acceptedValues.Add(key, value); + + object defaultValue; + if (_defaults != null && _defaults.TryGetValue(key, out defaultValue)) + { + if (RoutePartsEqual(value, defaultValue)) + { + _acceptedDefaultValues.Add(key); + } + } + } + } + + public void AcceptDefault(string key) + { + Contract.Assert(!_acceptedValues.ContainsKey(key)); + + object value; + if (_defaults != null && _defaults.TryGetValue(key, out value)) + { + _filters.Remove(key); + _acceptedValues.Add(key, value); + + _acceptedDefaultValues.Add(key); + } + } + + public bool NeedsValue(string key) + { + return !_acceptedValues.ContainsKey(key); + } + + public void Use(string key) + { + _unusedValues.Remove(key); + } + + private string DebuggerToString() + { + return string.Format( + "{{Accepted: '{0}' Filters: '{1}'}}", + string.Join(", ", _acceptedValues.Keys), + string.Join(", ", _filters.Keys)); + } + } + + [DebuggerDisplay("{DebuggerToString(),nq}")] + private class UriBuildingContext + { + // Holds the 'accepted' parts of the uri. + private readonly StringBuilder _uri; + + // Holds the 'optional' parts of the uri. We need a secondary buffer to handle cases where an optional + // segment is in the middle of the uri. We don't know whether or not we need to write it out - if it's + // followed by other optional segments than we will just throw it away. + private readonly StringBuilder _buffer; + + private bool _hasEmptySegment; + + public UriBuildingContext() + { + _uri = new StringBuilder(); + _buffer = new StringBuilder(); + + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + + } + + public SegmentState BufferState { get; private set; } + + public SegmentState UriState { get; private set; } + + public bool Accept(string value) + { + if (string.IsNullOrEmpty(value)) + { + if (UriState == SegmentState.Inside || BufferState == SegmentState.Inside) + { + // We can't write an 'empty' part inside a segment + return false; + } + else + { + _hasEmptySegment = true; + return true; + } + } + else if (_hasEmptySegment) + { + // We're trying to write text after an empty segment - this is not allowed. + return false; + } + + _uri.Append(_buffer); + _buffer.Clear(); + + if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + { + if (_uri.Length != 0) + { + _uri.Append("/"); + } + } + + BufferState = SegmentState.Inside; + UriState = SegmentState.Inside; + + _uri.Append(value); + return true; + } + + public bool Buffer(string value) + { + if (string.IsNullOrEmpty(value)) + { + if (BufferState == SegmentState.Inside) + { + // We can't write an 'empty' part inside a segment + return false; + } + else + { + _hasEmptySegment = true; + return true; + } + } + else if (_hasEmptySegment) + { + // We're trying to write text after an empty segment - this is not allowed. + return false; + } + + if (UriState == SegmentState.Inside) + { + // We've already written part of this segment so there's no point in buffering, we need to + // write out the rest or give up. + var result = Accept(value); + + // We've already checked the conditions that could result in a rejected part, so this should + // always be true. + Contract.Assert(result); + + return result; + } + + if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + { + if (_uri.Length != 0 || _buffer.Length != 0) + { + _buffer.Append("/"); + } + + BufferState = SegmentState.Inside; + } + + _buffer.Append(value); + return true; + } + + internal void EndSegment() + { + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + } + + internal string Build() + { + // We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'. + return _uri.ToString(); + } + + private string DebuggerToString() + { + return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri.ToString(), _buffer.ToString()); + } + } + + // Segments are treated as all-or-none. We should never output a partial segment. + // If we add any subsegment of this segment to the generated URI, we have to add + // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we + // used a value for {p1}, we have to output the entire segment up to the next "/". + // Otherwise we could end up with the partial segment "v1" instead of the entire + // segment "v1-v2.xml". + private enum SegmentState + { + Beginning, + Inside, + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs b/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs similarity index 93% rename from src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs rename to src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs index 94a32d38c9..05039dd6bb 100644 --- a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs @@ -2,31 +2,28 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.Contracts; -using System.Linq; namespace Microsoft.AspNet.Routing.Template { - [DebuggerDisplay("{DebuggerToString()}")] - public class ParsedTemplate + public class TemplateMatcher { private const string SeparatorString = "/"; private const char SeparatorChar = '/'; private static readonly char[] Delimiters = new char[] { SeparatorChar }; - public ParsedTemplate(List segments) + public TemplateMatcher(Template template) { - if (segments == null) + if (template == null) { - throw new ArgumentNullException("segments"); + throw new ArgumentNullException("template"); } - Segments = segments; + Template = template; } - public List Segments { get; private set; } + public Template Template { get; private set; } public IDictionary Match(string requestPath, IDictionary defaults) { @@ -41,7 +38,7 @@ namespace Microsoft.AspNet.Routing.Template for (int i = 0; i < requestSegments.Length; i++) { - var routeSegment = Segments.Count > i ? Segments[i] : null; + var routeSegment = Template.Segments.Count > i ? Template.Segments[i] : null; var requestSegment = requestSegments[i]; if (routeSegment == null) @@ -64,7 +61,7 @@ namespace Microsoft.AspNet.Routing.Template return null; } } - else + else { Contract.Assert(part.IsParameter); @@ -118,11 +115,11 @@ namespace Microsoft.AspNet.Routing.Template } } - for (int i = requestSegments.Length; i < Segments.Count; i++) + for (int i = requestSegments.Length; i < Template.Segments.Count; i++) { // We've matched the request path so far, but still have remaining route segments. These need // to be all single-part parameter segments with default values or else they won't match. - var routeSegment = Segments[i]; + var routeSegment = Template.Segments[i]; if (routeSegment.Parts.Count > 1) { // If it has more than one part it must contain literals, so it can't match. @@ -297,9 +294,5 @@ namespace Microsoft.AspNet.Routing.Template return (lastIndex == 0) || routeSegment.Parts[0].IsParameter; } - private string DebuggerToString() - { - return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString())); - } } } diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs index 3a74597f93..c8920bdefe 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNet.Routing.Template private const char EqualsSign = '='; private const char QuestionMark = '?'; - public static ParsedTemplate Parse(string routeTemplate) + public static Template Parse(string routeTemplate) { if (routeTemplate == null) { @@ -51,7 +51,7 @@ namespace Microsoft.AspNet.Routing.Template if (IsAllValid(context, segments)) { - return new ParsedTemplate(segments); + return new Template(segments); } else { diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index 4eab348596..e64338d49d 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Routing.Template { private readonly IDictionary _defaults; private readonly IRouteEndpoint _endpoint; - private readonly ParsedTemplate _parsedRoute; + private readonly Template _parsedTemplate; private readonly string _routeTemplate; public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate) @@ -25,11 +25,11 @@ namespace Microsoft.AspNet.Routing.Template } _endpoint = endpoint; - _routeTemplate = routeTemplate == null ? String.Empty : routeTemplate; + _routeTemplate = routeTemplate ?? String.Empty; _defaults = defaults ?? new Dictionary(StringComparer.OrdinalIgnoreCase); // The parser will throw for invalid routes. - _parsedRoute = TemplateParser.Parse(RouteTemplate); + _parsedTemplate = TemplateParser.Parse(RouteTemplate); } public IDictionary Defaults @@ -60,7 +60,7 @@ namespace Microsoft.AspNet.Routing.Template requestPath = requestPath.Substring(1); } - IDictionary values = _parsedRoute.Match(requestPath, _defaults); + IDictionary values = _parsedTemplate.Match(requestPath, _defaults); if (values == null) { // If we got back a null value set, that means the URI did not match diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs b/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs index 927b39a679..c5c02593b2 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.AspNet.Routing.Template.Tests { diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/RouteValueDictionary.cs b/test/Microsoft.AspNet.Routing.Tests/Template/RouteValueDictionary.cs new file mode 100644 index 0000000000..6a9a9a1f6b --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/RouteValueDictionary.cs @@ -0,0 +1,28 @@ + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + // This is just a placeholder + public class RouteValueDictionary : Dictionary + { + public RouteValueDictionary() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public RouteValueDictionary(object obj) + : base(StringComparer.OrdinalIgnoreCase) + { + if (obj != null) + { + foreach (var property in obj.GetType().GetTypeInfo().GetProperties()) + { + Add(property.Name, property.GetValue(obj)); + } + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs new file mode 100644 index 0000000000..218478d15e --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs @@ -0,0 +1,1056 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Xunit.Extensions; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class TemplateBinderTests + { + public static IEnumerable EmptyAndNullDefaultValues + { + get + { + return new object[][] + { + new object[] + { + "Test/{val1}/{val2}", + new RouteValueDictionary(new {val1 = "", val2 = ""}), + new RouteValueDictionary(new {val2 = "SomeVal2"}), + null, + }, + new object[] + { + "Test/{val1}/{val2}", + new RouteValueDictionary(new {val1 = "", val2 = ""}), + new RouteValueDictionary(new {val1 = "a"}), + "Test/a" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val3 = ""}), + new RouteValueDictionary(new {val2 = "a"}), + null + }, + new object[] + { + "Test/{val1}/{val2}", + new RouteValueDictionary(new {val1 = "", val2 = ""}), + new RouteValueDictionary(new {val1 = "a", val2 = "b"}), + "Test/a/b" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "a", val2 = "b", val3 = "c"}), + "Test/a/b/c" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "a", val2 = "b"}), + "Test/a/b" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "a"}), + "Test/a" + }, + new object[] + { + "Test/{val1}", + new RouteValueDictionary(new {val1 = "42", val2 = "", val3 = ""}), + new RouteValueDictionary(), + "Test" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "42", val2 = (string)null, val3 = (string)null}), + new RouteValueDictionary(), + "Test" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}/{val4}", + new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = "", val4 = ""}), + new RouteValueDictionary(new {val1 = "42", val2 = "11", val3 = "", val4 = ""}), + "Test/42/11" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "42"}), + "Test/42" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}/{val4}", + new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = "", val4 = ""}), + new RouteValueDictionary(new {val1 = "42", val2 = "11"}), + "Test/42/11" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "21", val2 = (string)null, val3 = (string)null}), + new RouteValueDictionary(new {val1 = "42"}), + "Test/42" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}/{val4}", + new RouteValueDictionary(new {val1 = "21", val2 = (string)null, val3 = (string)null, val4 = (string)null}), + new RouteValueDictionary(new {val1 = "42", val2 = "11"}), + "Test/42/11" + }, + }; + } + } + + [Theory] + [PropertyData("EmptyAndNullDefaultValues")] + public void Binding_WithEmptyAndNull_DefaultValues( + string template, + IDictionary defaults, + IDictionary values, + string expected) + { + // Arrange + var binder = new TemplateBinder(TemplateParser.Parse(template)); + + // Act + var boundTemplate = binder.Bind(defaults, null, values); + + // Assert + if (expected == null) + { + Assert.Null(boundTemplate); + } + else + { + Assert.NotNull(boundTemplate); + Assert.Equal(expected, boundTemplate.Path); + } + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}-{region}", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/xx-yy"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/xx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}-{region}", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/axx-yy"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/axx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "", region = "yy" }), + null); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "" }), + null); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/xx"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/xx-"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/axx"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}a", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/axxa"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentStandardMvcRouteMatches() + { + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "list", id = (string)null }), + new RouteValueDictionary(new { controller = "products" }), + "products.mvc"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + RunTest( + "language/{lang}-{region}", + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "zz" }), + "language/zz-yy"); + } + + [Fact] + public void GetUrlWithDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + RunTest( + "{controller}/{action}/{id}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { controller = "home", action = "oldaction" }), + new RouteValueDictionary(new { action = "newaction" }), + "home/newaction"); + } + + [Fact] + public void GetVirtualPathWithEmptyStringRequiredValueReturnsNull() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "" }), + null); + } + + [Fact] + public void GetVirtualPathWithNullRequiredValueReturnsNull() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = (string)null }), + null); + } + + [Fact] + public void GetVirtualPathWithRequiredValueReturnsPath() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "home" }), + "foo/home"); + } + + [Fact] + public void GetUrlWithNullDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + RunTest( + "{controller}/{action}/{id}", + new RouteValueDictionary(new { id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "oldaction", id = (string)null }), + new RouteValueDictionary(new { action = "newaction" }), + "home/newaction"); + } + + [Fact] + public void GetVirtualPathCanFillInSeparatedParametersWithDefaultValues() + { + RunTest( + "{controller}/{language}-{locale}", + new RouteValueDictionary(new { language = "en", locale = "US" }), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders" }), + "Orders/en-US"); + } + + [Fact] + public void GetVirtualPathWithUnusedNullValueShouldGenerateUrlAndIgnoreNullValue() + { + // DevDiv Bugs 194371: UrlRouting: Exception thrown when generating URL that has some null values + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { action = "Index", id = "" }), + new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }), + new RouteValueDictionary(new { controller = "Home", action = "TestAction", id = "1", format = (string)null }), + "Home.mvc/TestAction/1"); + } + + [Fact] + public void GetUrlWithMissingValuesDoesntMatch() + { + RunTest( + "{controller}/{action}/{id}", + null, + new { controller = "home", action = "oldaction" }, + new { action = "newaction" }, + null); + } + + [Fact] + public void GetUrlWithEmptyRequiredValuesReturnsNull() + { + RunTest( + "{p1}/{p2}/{p3}", + null, + new { p1 = "v1", }, + new { p2 = "", p3 = "" }, + null); + } + + [Fact] + public void GetUrlWithEmptyOptionalValuesReturnsShortUrl() + { + RunTest( + "{p1}/{p2}/{p3}", + new { p2 = "d2", p3 = "d3" }, + new { p1 = "v1", }, + new { p2 = "", p3 = "" }, + "v1"); + } + + [Fact] + public void GetUrlShouldIgnoreValuesAfterChangedParameter() + { + // DevDiv Bugs 157535 + RunTest( + "{controller}/{action}/{id}", + new { action = "Index", id = (string)null }, + new { controller = "orig", action = "init", id = "123" }, + new { action = "new", }, + "orig/new"); + } + + [Fact] + public void GetUrlWithNullForMiddleParameterIgnoresRemainingParameters() + { + // DevDiv Bugs 170859: UrlRouting: Passing null or empty string for a parameter in the middle of a route generates the wrong Url + RunTest( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }, + new { controller = "UrlRouting", action = "Play", category = "Photos", year = "2008", occasion = "Easter", SafeParam = "SafeParamValue" }, + new { year = (string)null, occasion = "Hola" }, + "UrlGeneration1/UrlRouting.mvc/Play/Photos/1995/Hola"); + } + + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterIgnoresRemainingParameters() + { + // DevDiv Bugs 170859: UrlRouting: Passing null or empty string for a parameter in the middle of a route generates the wrong Url + var ambientValues = new RouteValueDictionary(); + ambientValues.Add("controller", "UrlRouting"); + ambientValues.Add("action", "Play"); + ambientValues.Add("category", "Photos"); + ambientValues.Add("year", "2008"); + ambientValues.Add("occasion", "Easter"); + ambientValues.Add("SafeParam", "SafeParamValue"); + + var values = new RouteValueDictionary(); + values.Add("year", String.Empty); + values.Add("occasion", "Hola"); + + RunTest( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }), + ambientValues, + values, + "UrlGeneration1/UrlRouting.mvc/Play/Photos/1995/Hola"); + } + + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterShouldUseDefaultValue() + { + // DevDiv Bugs 172084: UrlRouting: Route.GetUrl generates the wrong route of new values has a different controller and route has an action parameter with default + var ambientValues = new RouteValueDictionary(); + ambientValues.Add("Controller", "Test"); + ambientValues.Add("Action", "Fallback"); + ambientValues.Add("param1", "fallback1"); + ambientValues.Add("param2", "fallback2"); + ambientValues.Add("param3", "fallback3"); + + var values = new RouteValueDictionary(); + values.Add("controller", "subtest"); + values.Add("param1", "b"); + // The original bug for this included this value, but with the new support for + // creating query string values it changes the behavior such that the URL is + // not what was originally expected. To preserve the general behavior of this + // unit test the 'param2' value is no longer being added. + //values.Add("param2", "a"); + + RunTest( + "{controller}.mvc/{action}/{param1}", + new RouteValueDictionary(new { action = "Default" }), + ambientValues, + values, + "subtest.mvc/Default/b"); + } + + [Fact] + public void GetUrlVerifyEncoding() + { + var values = new RouteValueDictionary(); + values.Add("controller", "#;?:@&=+$,"); + values.Add("action", "showcategory"); + values.Add("id", 123); + values.Add("so?rt", "de?sc"); + values.Add("maxPrice", 100); + + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), + values, + "%23%3b%3f%3a%40%26%3d%2b%24%2c.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100"); + } + + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesAndEscapesQueryString() + { + var values = new RouteValueDictionary(new { controller = "products", action = "showcategory", id = 123, maxPrice = 100 }); + values.Add("so?rt", "de?sc"); + + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), + values, + "products.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100"); + } + + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesButIgnoresNewValuesThatMatchDefaults() + { + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home", Custom = "customValue" }), + new RouteValueDictionary(new { controller = "Home", action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "products", action = "showcategory", id = 123, sort = "desc", maxPrice = 100, custom = "customValue" }), + "products.mvc/showcategory/123?sort=desc&maxPrice=100"); + } + + [Fact] + public void GetVirtualPathEncodesParametersAndLiterals() + { + RunTest( + "bl%og/{controller}/he llo/{action}", + null, + new RouteValueDictionary(new { controller = "ho%me", action = "li st" }), + new RouteValueDictionary(), + "bl%25og/ho%25me/he%20llo/li%20st"); + } + + [Fact] + public void GetUrlWithCatchAllWithValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = "v2a/v2b" }), + "v1/v2a/v2b"); + } + + [Fact] + public void GetUrlWithCatchAllWithEmptyValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = "" }), + "v1"); + } + + [Fact] + public void GetUrlWithCatchAllWithNullValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = (string)null }), + "v1"); + } + +#if ROUTE_COLLECTION + + [Fact] + public void GetUrlShouldValidateOnlyAcceptedParametersAndUserDefaultValuesForInvalidatedParameters() + { + // DevDiv Bugs 172913: UrlRouting: Parameter validation should not run against current request values if a new value has been supplied at a previous position + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("Controller", "UrlRouting"); + rd.Values.Add("Name", "MissmatchedValidateParams"); + rd.Values.Add("action", "MissmatchedValidateParameters2"); + rd.Values.Add("ValidateParam1", "special1"); + rd.Values.Add("ValidateParam2", "special2"); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", + new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams", ValidateParam2 = "valid" }), + new RouteValueDictionary(new { ValidateParam1 = "valid.*", ValidateParam2 = "valid.*" }))); + + rc.Add(CreateRoute( + "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", + new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams" }), + new RouteValueDictionary(new { ValidateParam1 = "special.*", ValidateParam2 = "special.*" }))); + + var values = CreateRouteValueDictionary(); + values.Add("Name", "MissmatchedValidateParams"); + values.Add("ValidateParam1", "valid1"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/UrlConstraints/Validation.mvc/Input5/MissmatchedValidateParameters2/valid1", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithRouteThatHasExtensionWithSubsequentDefaultValueIncludesExtensionButNotDefaultValue() + { + // DevDiv Bugs 156606 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bank"); + rd.Values.Add("action", "MakeDeposit"); + rd.Values.Add("accountId", "7770"); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "{controller}.mvc/Deposit/{accountId}", + new RouteValueDictionary(new { Action = "DepositView" }))); + + // Note: This route was in the original bug, but it turns out that this behavior is incorrect. With the + // recent fix to Route (in this changelist) this route would have been selected since we have values for + // all three required parameters. + //rc.Add(new Route { + // Url = "{controller}.mvc/{action}/{accountId}", + // RouteHandler = new DummyRouteHandler() + //}); + + // This route should be chosen because the requested action is List. Since the default value of the action + // is List then the Action should not be in the URL. However, the file extension should be included since + // it is considered "safe." + rc.Add(CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { Action = "List" }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "List"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Bank.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithRouteThatHasDifferentControllerCaseShouldStillMatch() + { + // DevDiv Bugs 159099 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bar"); + rd.Values.Add("action", "bbb"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute("PrettyFooUrl", new RouteValueDictionary(new { controller = "Foo", action = "aaa", id = (string)null }))); + + rc.Add(CreateRoute("PrettyBarUrl", new RouteValueDictionary(new { controller = "Bar", action = "bbb", id = (string)null }))); + + rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "aaa"); + values.Add("Controller", "foo"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/PrettyFooUrl", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithNoChangedValuesShouldProduceSameUrl() + { + // DevDiv Bugs 159469 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "Index"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlAppliesConstraintsRulesToChooseRoute() + { + // DevDiv Bugs 159678: MVC: URL generation chooses the wrong route for generating URLs when route validation is in place + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "foo.mvc/{action}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "Home", action = "Contact", httpMethod = CreateHttpMethodConstraint("get") }))); + + rc.Add(CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { action = "Index" }), + new RouteValueDictionary(new { controller = "Home", action = "(Index|About)", httpMethod = CreateHttpMethodConstraint("post") }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "Index"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRoute() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + IRouteCollection rt = new DefaultRouteCollection(); + rt.Add(CreateRoute("date/{y}/{m}/{d}", null)); + rt.Add(CreateRoute("{controller}/{action}/{id}", null)); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "dostuff"); + + var values = CreateRouteValueDictionary(); + values.Add("y", "2007"); + values.Add("m", "08"); + values.Add("d", "12"); + + // Act + var vpd = rt.GetVirtualPath(context, values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRouteAsSecondRoute() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + + IRouteCollection rt = new DefaultRouteCollection(); + rt.Add(CreateRoute("{controller}/{action}/{id}")); + rt.Add(CreateRoute("date/{y}/{m}/{d}")); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "dostuff"); + + var values = CreateRouteValueDictionary(); + values.Add("y", "2007"); + values.Add("m", "08"); + values.Add("d", "12"); + + // Act + var vpd = rt.GetVirtualPath(context, values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); + } + + [Fact] + public void GetVirtualPathUsesCurrentValuesNotInRouteToMatch() + { + // DevDiv Bugs 177401: UrlRouting: Incorrect route picked on urlgeneration if using controller from ambient values and route does not have a url parameter for controller + + // DevDiv Bugs 191162: UrlRouting: Route does not match when an ambient route value doesn't match a required default value in the target route + // Because of this bug the test was split into two separate verifications since the original test was verifying slightly incorrect behavior + + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r1 = CreateRoute( + "ParameterMatching.mvc/{Action}/{product}", + new RouteValueDictionary(new { Controller = "ParameterMatching", product = (string)null }), + null); + + TemplateRoute r2 = CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { Action = "List" }), + new RouteValueDictionary(new { Controller = "Action|Bank|Overridden|DerivedFromAction|OverrideInvokeActionAndExecute|InvalidControllerName|Store|HtmlHelpers|(T|t)est|UrlHelpers|Custom|Parent|Child|TempData|ViewFactory|LocatingViews|AccessingDataInViews|ViewOverrides|ViewMasterPage|InlineCompileError|CustomView" }), + null); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bank"); + rd.Values.Add("Action", "List"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("action", "AttemptLogin"); + + // Act for first route + var vpd = r1.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("ParameterMatching.mvc/AttemptLogin", vpd.VirtualPath); + + // Act for second route + vpd = r2.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("Bank.mvc/AttemptLogin", vpd.VirtualPath); + } + +#endif + +#if DATA_TOKENS + [Fact] + public void GetVirtualPathWithDataTokensCopiesThemFromRouteToVirtualPathData() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{controller}/{action}", null, null, new RouteValueDictionary(new { foo = "bar", qux = "quux" })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "index"); + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("home/index", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.Equal(2, vpd.DataTokens.Count); + Assert.Equal("bar", vpd.DataTokens["foo"]); + Assert.Equal("quux", vpd.DataTokens["qux"]); + } +#endif + +#if ROUTE_FORMAT_HELPER + + [Fact] + public void UrlWithEscapedOpenCloseBraces() + { + RouteFormatHelper("foo/{{p1}}", "foo/{p1}"); + } + + [Fact] + public void UrlWithEscapedOpenBraceAtTheEnd() + { + RouteFormatHelper("bar{{", "bar{"); + } + + [Fact] + public void UrlWithEscapedOpenBraceAtTheBeginning() + { + RouteFormatHelper("{{bar", "{bar"); + } + + [Fact] + public void UrlWithRepeatedEscapedOpenBrace() + { + RouteFormatHelper("foo{{{{bar", "foo{{bar"); + } + + [Fact] + public void UrlWithEscapedCloseBraceAtTheEnd() + { + RouteFormatHelper("bar}}", "bar}"); + } + + [Fact] + public void UrlWithEscapedCloseBraceAtTheBeginning() + { + RouteFormatHelper("}}bar", "}bar"); + } + + [Fact] + public void UrlWithRepeatedEscapedCloseBrace() + { + RouteFormatHelper("foo}}}}bar", "foo}}bar"); + } + + private static void RouteFormatHelper(string routeUrl, string requestUrl) + { + var defaults = new RouteValueDictionary(new { route = "matched" }); + var r = CreateRoute(routeUrl, defaults, null); + + GetRouteDataHelper(r, requestUrl, defaults); + GetVirtualPathHelper(r, new RouteValueDictionary(), null, Uri.EscapeUriString(requestUrl)); + } + +#endif + +#if CONSTRAINTS + [Fact] + public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString() + { + // DevDiv Bugs 199612: UrlRouting: UrlGeneration should not append parameter to query string if it is a Constraint parameter and not a Url parameter + RunTest( + "{Controller}.mvc/{action}/{end}", + null, + new RouteValueDictionary(new { foo = CreateHttpMethodConstraint("GET") }), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders", action = "Index", end = "end", foo = "GET" }), + "Orders.mvc/Index/end"); + } + + [Fact] + public void GetVirtualPathWithValidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "index"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("home/index", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("index", r.ConstraintData.ParameterValue); + } + + [Fact] + public void GetVirtualPathWithInvalidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "list"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("list", r.ConstraintData.ParameterValue); + } + +#endif + + private static void RunTest( + string template, + IDictionary defaults, + IDictionary ambientValues, + IDictionary values, + string expected) + { + // Arrange + var binder = new TemplateBinder(TemplateParser.Parse(template)); + + // Act + var boundTemplate = binder.Bind(defaults, ambientValues, values); + + // Assert + if (expected == null) + { + Assert.Null(boundTemplate); + } + else + { + Assert.NotNull(boundTemplate); + + // We want to chop off the query string and compare that using an unordered comparison + var expectedParts = new PathAndQuery(expected); + var actualParts = new PathAndQuery(boundTemplate.Path); + + Assert.Equal(expectedParts.Path, actualParts.Path); + + if (expectedParts.Parameters == null) + { + Assert.Null(actualParts.Parameters); + } + else + { + Assert.Equal(expectedParts.Parameters.Count, actualParts.Parameters.Count); + + foreach (var kvp in expectedParts.Parameters) + { + string value; + Assert.True(actualParts.Parameters.TryGetValue(kvp.Key, out value)); + Assert.Equal(kvp.Value, value); + } + } + } + } + + private static void RunTest( + string template, + object defaults, + object ambientValues, + object values, + string expected) + { + RunTest( + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(ambientValues), + new RouteValueDictionary(values), + expected); + } + + private class PathAndQuery + { + public PathAndQuery(string uri) + { + var queryIndex = uri.IndexOf("?", StringComparison.Ordinal); + if (queryIndex == -1) + { + Path = uri; + } + else + { + Path = uri.Substring(0, queryIndex); + + var query = uri.Substring(queryIndex + 1); + Parameters = + query + .Split(new char[] { '&' }, StringSplitOptions.None) + .Select(s => s.Split(new char[] { '=' }, StringSplitOptions.None)) + .ToDictionary(pair => pair[0], pair => pair[1]); + } + } + + public string Path { get; private set; } + + public Dictionary Parameters { get; private set; } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs new file mode 100644 index 0000000000..b445e31e8c --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs @@ -0,0 +1,783 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Xunit; +using Xunit.Extensions; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class TemplateMatcherTests + { + [Fact] + public void MatchSingleRoute() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); + + // Act + var match = matcher.Match("Bank/DoAction/123", null); + + // Assert + Assert.NotNull(match); + Assert.Equal("Bank", match["controller"]); + Assert.Equal("DoAction", match["action"]); + Assert.Equal("123", match["id"]); + } + + [Fact] + public void NoMatchSingleRoute() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); + + // Act + var match = matcher.Match("Bank/DoAction", null); + + // Assert + Assert.Null(match); + } + + [Fact] + public void MatchSingleRouteWithDefaults() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); + + // Act + var rd = matcher.Match("Bank/DoAction", new RouteValueDictionary(new { id = "default id" })); + + // Assert + Assert.Equal("Bank", rd["controller"]); + Assert.Equal("DoAction", rd["action"]); + Assert.Equal("default id", rd["id"]); + } + + [Fact] + public void NoMatchSingleRouteWithDefaults() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); + + // Act + var rd = matcher.Match("Bank", new RouteValueDictionary(new { id = "default id" })); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchRouteWithLiterals() + { + // Arrange + var matcher = CreateMatcher("moo/{p1}/bar/{p2}"); + + // Act + var rd = matcher.Match("moo/111/bar/222", new RouteValueDictionary(new { p2 = "default p2" })); + + // Assert + Assert.Equal("111", rd["p1"]); + Assert.Equal("222", rd["p2"]); + } + + [Fact] + public void MatchRouteWithLiteralsAndDefaults() + { + // Arrange + var matcher = CreateMatcher("moo/{p1}/bar/{p2}"); + + // Act + var rd = matcher.Match("moo/111/bar/", new RouteValueDictionary(new { p2 = "default p2" })); + + // Assert + Assert.Equal("111", rd["p1"]); + Assert.Equal("default p2", rd["p2"]); + } + + [Fact] + public void MatchRouteWithOnlyLiterals() + { + // Arrange + var matcher = CreateMatcher("moo/bar"); + + // Act + var rd = matcher.Match("moo/bar", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Count); + } + + [Fact] + public void NoMatchRouteWithOnlyLiterals() + { + // Arrange + var matcher = CreateMatcher("moo/bars"); + + // Act + var rd = matcher.Match("moo/bar", null); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchRouteWithExtraSeparators() + { + // Arrange + var matcher = CreateMatcher("moo/bar"); + + // Act + var rd = matcher.Match("moo/bar/", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Count); + } + + [Fact] + public void MatchRouteUrlWithExtraSeparators() + { + // Arrange + var matcher = CreateMatcher("moo/bar/"); + + // Act + var rd = matcher.Match("moo/bar", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Count); + } + + [Fact] + public void MatchRouteUrlWithParametersAndExtraSeparators() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}/"); + + // Act + var rd = matcher.Match("moo/bar", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal("moo", rd["p1"]); + Assert.Equal("bar", rd["p2"]); + } + + [Fact] + public void NoMatchRouteUrlWithDifferentLiterals() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}/baz"); + + // Act + var rd = matcher.Match("moo/bar/boo", null); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void NoMatchLongerUrl() + { + // Arrange + var matcher = CreateMatcher("{p1}"); + + // Act + var rd = matcher.Match("moo/bar", null); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchSimpleFilename() + { + // Arrange + var matcher = CreateMatcher("DEFAULT.ASPX"); + + // Act + var rd = matcher.Match("default.aspx", null); + + // Assert + Assert.NotNull(rd); + } + + [Theory] + [InlineData("{prefix}x{suffix}", "xxxxxxxxxx")] + [InlineData("{prefix}xyz{suffix}", "xxxxyzxyzxxxxxxyz")] + [InlineData("{prefix}xyz{suffix}", "abcxxxxyzxyzxxxxxxyzxx")] + [InlineData("{prefix}xyz{suffix}", "xyzxyzxyzxyzxyz")] + [InlineData("{prefix}xyz{suffix}", "xyzxyzxyzxyzxyz1")] + [InlineData("{prefix}xyz{suffix}", "xyzxyzxyz")] + [InlineData("{prefix}aa{suffix}", "aaaaa")] + [InlineData("{prefix}aaa{suffix}", "aaaaa")] + public void VerifyRouteMatchesWithContext(string template, string path) + { + var matcher = CreateMatcher(template); + + // Act + var rd = matcher.Match(path, null); + + // Assert + Assert.NotNull(rd); + } + + [Fact] + public void MatchRouteWithExtraDefaultValues() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}"); + + // Act + var rd = matcher.Match("v1", new RouteValueDictionary(new { p2 = (string)null, foo = "bar" })); + + // Assert + Assert.NotNull(rd); + Assert.Equal(3, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Null(rd["p2"]); + Assert.Equal("bar", rd["foo"]); + } + + [Fact] + public void MatchPrettyRouteWithExtraDefaultValues() + { + // Arrange + var matcher = CreateMatcher("date/{y}/{m}/{d}"); + + // Act + var rd = matcher.Match("date/2007/08", new RouteValueDictionary(new { controller = "blog", action = "showpost", m = (string)null, d = (string)null })); + + // Assert + Assert.NotNull(rd); + Assert.Equal(5, rd.Count); + Assert.Equal("blog", rd["controller"]); + Assert.Equal("showpost", rd["action"]); + Assert.Equal("2007", rd["y"]); + Assert.Equal("08", rd["m"]); + Assert.Null(rd["d"]); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}-{region}", + "language/en-US", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-{region}a", + "language/en-USa", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}-{region}", + "language/aen-US", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}-{region}a", + "language/aen-USa", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + RunTest( + "language/a{lang}-{region}a", + "language/a-USa", + null, + null); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + RunTest( + "language/a{lang}-{region}a", + "language/aen-a", + null, + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}", + "language/en", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsTrailingSlashDoesNotMatch() + { + RunTest( + "language/{lang}", + "language/", + null, + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsDoesNotMatch() + { + RunTest( + "language/{lang}", + "language", + null, + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-", + "language/en-", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}", + "language/aen", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}a", + "language/aena", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentStandardMvcRouteMatches() + { + RunTest( + "{controller}.mvc/{action}/{id}", + "home.mvc/index", + new RouteValueDictionary(new { action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + RunTest( + "language/{lang}-{region}", + "language/-", + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + null); + } + + [Fact] + public void GetRouteDataWithUrlWithMultiSegmentWithRepeatedDots() + { + RunTest( + "{Controller}..mvc/{id}/{Param1}", + "Home..mvc/123/p1", + null, + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } + + [Fact] + public void GetRouteDataWithUrlWithTwoRepeatedDots() + { + RunTest( + "{Controller}.mvc/../{action}", + "Home.mvc/../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithThreeRepeatedDots() + { + RunTest( + "{Controller}.mvc/.../{action}", + "Home.mvc/.../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithManyRepeatedDots() + { + RunTest( + "{Controller}.mvc/../../../{action}", + "Home.mvc/../../../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithExclamationPoint() + { + RunTest( + "{Controller}.mvc!/{action}", + "Home.mvc!/index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithStartingDotDotSlash() + { + RunTest( + "../{Controller}.mvc", + "../Home.mvc", + null, + new RouteValueDictionary(new { Controller = "Home" })); + } + + [Fact] + public void GetRouteDataWithUrlWithStartingBackslash() + { + RunTest( + @"\{Controller}.mvc", + @"\Home.mvc", + null, + new RouteValueDictionary(new { Controller = "Home" })); + } + + [Fact] + public void GetRouteDataWithUrlWithBackslashSeparators() + { + RunTest( + @"{Controller}.mvc\{id}\{Param1}", + @"Home.mvc\123\p1", + null, + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } + + [Fact] + public void GetRouteDataWithUrlWithParenthesesLiterals() + { + RunTest( + @"(Controller).mvc", + @"(Controller).mvc", + null, + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithUrlWithTrailingSlashSpace() + { + RunTest( + @"Controller.mvc/ ", + @"Controller.mvc/ ", + null, + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithUrlWithTrailingSpace() + { + RunTest( + @"Controller.mvc ", + @"Controller.mvc ", + null, + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithCatchAllCapturesDots() + { + // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "." + RunTest( + "Home/ShowPilot/{missionId}/{*name}", + "Home/ShowPilot/777/12345./foobar", + new RouteValueDictionary(new + { + controller = "Home", + action = "ShowPilot", + missionId = (string)null, + name = (string)null + }), + new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" })); + } + + [Fact] + public void RouteWithCatchAllClauseCapturesManySlashes() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + // Act + var rd = matcher.Match("v1/v2/v3", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Equal("v2/v3", rd["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseCapturesTrailingSlash() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + // Act + var rd = matcher.Match("v1/", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Null(rd["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseCapturesEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + // Act + var rd = matcher.Match("v1", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Null(rd["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseUsesDefaultValueForEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + // Act + var rd = matcher.Match("v1", new RouteValueDictionary(new { p2 = "catchall" })); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Equal("catchall", rd["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseIgnoresDefaultValueForNonEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + // Act + var rd = matcher.Match("v1/hello/whatever", new RouteValueDictionary(new { p2 = "catchall" })); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Equal("hello/whatever", rd["p2"]); + } + + [Fact] + public void GetRouteDataDoesNotMatchOnlyLeftLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "fooBAR", + null, + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchOnlyRightLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "BARfoo", + null, + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchMiddleLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "BARfooBAR", + null, + null); + } + + [Fact] + public void GetRouteDataDoesMatchesExactLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "foo", + null, + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithWeirdParameterNames() + { + RunTest( + "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", + "foo/space/weird/orderid", + new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, + new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weird" }, { "dynamic.data", "orderid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); + } + + [Fact] + public void GetRouteDataDoesNotMatchRouteWithLiteralSeparatorDefaultsButNoValue() + { + RunTest( + "{controller}/{language}-{locale}", + "foo", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchesRouteWithLiteralSeparatorDefaultsAndLeftValue() + { + RunTest( + "{controller}/{language}-{locale}", + "foo/xx-", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchesRouteWithLiteralSeparatorDefaultsAndRightValue() + { + RunTest( + "{controller}/{language}-{locale}", + "foo/-yy", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } + + [Fact] + public void GetRouteDataMatchesRouteWithLiteralSeparatorDefaultsAndValue() + { + RunTest( + "{controller}/{language}-{locale}", + "foo/xx-yy", + new RouteValueDictionary(new { language = "en", locale = "US" }), + new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); + } + + [Fact] + public void MatchSetsOptionalParameter() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}"); + var url = "Home/Index"; + + // Act + var match = route.Match(url, null); + + // Assert + Assert.NotNull(match); + Assert.Equal(2, match.Values.Count); + Assert.Equal("Home", match["controller"]); + Assert.Equal("Index", match["action"]); + } + + [Fact] + public void MatchDoesNotSetOptionalParameter() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}"); + var url = "Home"; + + // Act + var match = route.Match(url, null); + + // Assert + Assert.NotNull(match); + Assert.Equal(1, match.Values.Count); + Assert.Equal("Home", match["controller"]); + Assert.False(match.ContainsKey("action")); + } + + [Fact] + public void MatchMultipleOptionalParameters() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}/{id?}"); + var url = "Home/Index"; + + // Act + var match = route.Match(url, null); + + // Assert + Assert.NotNull(match); + Assert.Equal(2, match.Values.Count); + Assert.Equal("Home", match["controller"]); + Assert.Equal("Index", match["action"]); + Assert.False(match.ContainsKey("id")); + } + + private TemplateMatcher CreateMatcher(string template) + { + return new TemplateMatcher(TemplateParser.Parse(template)); + } + + private static void RunTest(string template, string path, IDictionary defaults, IDictionary expected) + { + // Arrange + var matcher = new TemplateMatcher(TemplateParser.Parse(template)); + + // Act + var match = matcher.Match(path, defaults); + + // Assert + if (expected == null) + { + Assert.Null(match); + } + else + { + Assert.NotNull(match); + Assert.Equal(expected.Count, match.Values.Count); + foreach (string key in match.Keys) + { + Assert.Equal(expected[key], match[key]); + } + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs index 9140836284..24074722bb 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests // Arrange var template = "cool"; - var expected = new ParsedTemplate(new List()); + var expected = new Template(new List()); expected.Segments.Add(new TemplateSegment()); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); @@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests var actual = TemplateParser.Parse(template); // Assert - Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + Assert.Equal