Skip to content

Commit

Permalink
Merge pull request tomkuijsten#93 from Jark/feature-add-validation-fo…
Browse files Browse the repository at this point in the history
…r-unique-urls

Added validation for unique URIs
  • Loading branch information
tomkuijsten authored Oct 12, 2016
2 parents 8d831e1 + 8908696 commit 9079ffe
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using Restup.HttpMessage.Models.Schemas;
using Restup.Webserver.Attributes;
using Restup.Webserver.Models.Contracts;
using Restup.Webserver.Models.Schemas;
using Restup.Webserver.Rest;
using Restup.Webserver.UnitTests.TestHelpers;
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Restup.Webserver.UnitTests.Rest
{
[TestClass]
public class RestRouteHandlerTests_UriFormatValidation
{
private RestRouteHandler restHandler;

[TestInitialize()]
public void Initialize()
{
restHandler = new RestRouteHandler();
}

private class TwoUriFormatWithSameNameAndMethodController
{
[UriFormat("/Get")]
public IGetResponse Get() => new GetResponse(GetResponse.ResponseStatus.OK);

[UriFormat("/Get")]
public IGetResponse Get2() => new GetResponse(GetResponse.ResponseStatus.OK);
}

[TestMethod]
public async Task RegisterController_OneControllerWithTwoMethodsWithSameNameAndMethod_ThrowsException()
{
AssertRegisterControllerThrows<TwoUriFormatWithSameNameAndMethodController>();
await AssertHandleRequest("/Get", HttpMethod.GET, HttpResponseStatus.BadRequest);
}

private class OnePostMethodController
{
[UriFormat("/Post")]
public IPostResponse Post() => new PostResponse(PostResponse.ResponseStatus.Created);
}

[TestMethod]
public async Task RegisterController_OneControllerRegisteredTwice_ThrowsException()
{
restHandler.RegisterController<OnePostMethodController>();
await AssertHandleRequest("/Post", HttpMethod.POST, HttpResponseStatus.Created);
AssertRegisterControllerThrows<OnePostMethodController>();
await AssertHandleRequest("/Post", HttpMethod.POST, HttpResponseStatus.Created);
}

private class OnePostMethodWithParameterizedConstructorController
{
[UriFormat("/Post")]
public IPostResponse Post() => new PostResponse(PostResponse.ResponseStatus.Created);

public OnePostMethodWithParameterizedConstructorController(string param)
{
}
}

[TestMethod]
public async Task RegisterController_OneParameterizedControllerRegisteredTwice_ThrowsException()
{
restHandler.RegisterController<OnePostMethodWithParameterizedConstructorController>("param");
await AssertHandleRequest("/Post", HttpMethod.POST, HttpResponseStatus.Created);
AssertRegisterControllerThrows<OnePostMethodWithParameterizedConstructorController>("param");
await AssertHandleRequest("/Post", HttpMethod.POST, HttpResponseStatus.Created);
}

[TestMethod]
public async Task RegisterController_TwoDifferentControllersWithSimilarlyNamedMethodsAndVerbs_ThrowsException()
{
restHandler.RegisterController<OnePostMethodController>();
await AssertHandleRequest("/Post", HttpMethod.POST, HttpResponseStatus.Created);
AssertRegisterControllerThrows<OnePostMethodWithParameterizedConstructorController>("param");
await AssertHandleRequest("/Post", HttpMethod.POST, HttpResponseStatus.Created);
}

private void AssertRegisterControllerThrows<T>(params string[] args) where T : class
{
Assert.ThrowsException<Exception>(() =>
restHandler.RegisterController<T>(args)
);
}

private void AssertRegisterControllerThrows<T>() where T : class
{
Assert.ThrowsException<Exception>(() =>
restHandler.RegisterController<T>()
);
}

private async Task AssertHandleRequest(string uri, HttpMethod method, HttpResponseStatus expectedStatus)
{
var request = Utils.CreateHttpRequest(uri: new Uri(uri, UriKind.Relative), method: method);
var result = await restHandler.HandleRequest(request);

Assert.AreEqual(expectedStatus, result.ResponseStatus);
}
}
}
3 changes: 2 additions & 1 deletion src/WebServer.UnitTests/WebServer.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<AssemblyName>Restup.Webserver.UnitTests</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion>10.0.10240.0</TargetPlatformVersion>
<TargetPlatformVersion>10.0.10586.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.10240.0</TargetPlatformMinVersion>
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
<FileAlignment>512</FileAlignment>
Expand Down Expand Up @@ -98,6 +98,7 @@
<Compile Include="File\MimeTypeProviderTests.cs" />
<Compile Include="Http\HttpServerTests.CorsSimpleRequests.cs" />
<Compile Include="Http\HttpServerTests.CorsPreflightedRequests.cs" />
<Compile Include="Rest\RestRouteHandlerTests.UriFormatValidation.cs" />
<Compile Include="TestHelpers\MockFile.cs" />
<Compile Include="TestHelpers\MockFileSystem.cs" />
<Compile Include="TestHelpers\StaticFileRouteHandlerTests.Fluent.cs" />
Expand Down
7 changes: 7 additions & 0 deletions src/WebServer/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Restup.WebServer
{
internal class Constants
{
public const int HashCodePrime = 397;
}
}
24 changes: 24 additions & 0 deletions src/WebServer/Rest/PathPart.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System;
using Restup.WebServer;

namespace Restup.Webserver.Rest
{
public class PathPart
Expand All @@ -16,5 +19,26 @@ public PathPart(PathPartType pathPartType, string value)
PartType = pathPartType;
Value = value;
}

protected bool Equals(PathPart other)
{
return PartType == other.PartType && string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
}

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((PathPart)obj);
}

public override int GetHashCode()
{
unchecked
{
return ((int)PartType * Constants.HashCodePrime) ^ (Value?.GetHashCode() ?? 0);
}
}
}
}
18 changes: 9 additions & 9 deletions src/WebServer/Rest/RestControllerMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ internal enum TypeWrapper
private readonly IEnumerable<Type> _validParameterTypes;

private readonly UriParser _uriParser;
private readonly ParsedUri _matchUri;
private readonly IEnumerable<ParameterValueGetter> _parameterGetters;

internal ParsedUri MatchUri { get; }
internal MethodInfo MethodInfo { get; }
internal HttpMethod Verb { get; }
internal bool HasContentParameter { get; }
Expand All @@ -37,7 +37,7 @@ internal RestControllerMethodInfo(
{
constructorArgs.GuardNull(nameof(constructorArgs));
_uriParser = new UriParser();
_matchUri = GetUriFromMethod(methodInfo);
MatchUri = GetUriFromMethod(methodInfo);

ReturnTypeWrapper = typeWrapper;
ControllerConstructorArgs = constructorArgs;
Expand Down Expand Up @@ -121,7 +121,7 @@ where p.GetCustomAttribute<FromContentAttribute>() == null
throw new InvalidOperationException("Can't use method parameters with a custom type.");
}

return fromUriParams.Select(x => GetParameterGetter(x, _matchUri)).ToArray();
return fromUriParams.Select(x => GetParameterGetter(x, MatchUri)).ToArray();
}

private static ParameterValueGetter GetParameterGetter(ParameterInfo parameterInfo, ParsedUri matchUri)
Expand Down Expand Up @@ -183,12 +183,12 @@ private static bool IsRestResponseOfType<T>(TypeInfo returnType)

internal bool Match(ParsedUri uri)
{
if (_matchUri.PathParts.Count != uri.PathParts.Count)
if (MatchUri.PathParts.Count != uri.PathParts.Count)
return false;

for (var i = 0; i < _matchUri.PathParts.Count; i++)
for (var i = 0; i < MatchUri.PathParts.Count; i++)
{
var fromPart = _matchUri.PathParts[i];
var fromPart = MatchUri.PathParts[i];
var toPart = uri.PathParts[i];
if (fromPart.PartType == PathPart.PathPartType.Argument)
continue;
Expand All @@ -197,10 +197,10 @@ internal bool Match(ParsedUri uri)
return false;
}

if (uri.Parameters.Count < _matchUri.Parameters.Count)
if (uri.Parameters.Count < MatchUri.Parameters.Count)
return false;

return _matchUri.Parameters.All(x => uri.Parameters.Any(y => y.Name.Equals(x.Name, StringComparison.OrdinalIgnoreCase)));
return MatchUri.Parameters.All(x => uri.Parameters.Any(y => y.Name.Equals(x.Name, StringComparison.OrdinalIgnoreCase)));
}

internal IEnumerable<object> GetParametersFromUri(ParsedUri uri)
Expand All @@ -210,7 +210,7 @@ internal IEnumerable<object> GetParametersFromUri(ParsedUri uri)

public override string ToString()
{
return $"Hosting {Verb} method on {_matchUri}";
return $"Hosting {Verb} method on {MatchUri}";
}
}
}
33 changes: 33 additions & 0 deletions src/WebServer/Rest/RestControllerMethodInfoValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace Restup.Webserver.Rest
{
internal class RestControllerMethodInfoValidator
{
private readonly UniqueMatchUriAndVerbRestControllerMethodInfoComparer _uniqueMatchUriAndVerbComparer;

public RestControllerMethodInfoValidator()
{
_uniqueMatchUriAndVerbComparer = new UniqueMatchUriAndVerbRestControllerMethodInfoComparer();
}

public void Validate<T>(ImmutableArray<RestControllerMethodInfo> existingRestMethodCollection,
IList<RestControllerMethodInfo> restControllerMethodInfos)
{
foreach (var restControllerMethodInfo in restControllerMethodInfos)
{
// if the existing rest method collection already contains the rest controller method to be added
// or if the rest controller method infos to be added contains more than one rest method info with the same path
// then throw an exception
if (existingRestMethodCollection.Contains(restControllerMethodInfo, _uniqueMatchUriAndVerbComparer)
|| restControllerMethodInfos.Count(x => _uniqueMatchUriAndVerbComparer.Equals(x, restControllerMethodInfo)) > 1)
{
throw new Exception($"Can't register route for controller {typeof(T)}, UriFormat with {restControllerMethodInfo.MatchUri} and {restControllerMethodInfo.Verb} since this would cause multiple routes to be registered on the same name.");
}
}
}
}
}
12 changes: 9 additions & 3 deletions src/WebServer/Rest/RestControllerRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ internal class RestControllerRequestHandler
private ImmutableArray<RestControllerMethodInfo> _restMethodCollection;
private readonly RestResponseFactory _responseFactory;
private readonly RestControllerMethodExecutorFactory _methodExecuteFactory;
private UriParser _uriParser;
private readonly UriParser _uriParser;
private readonly RestControllerMethodInfoValidator _restControllerMethodInfoValidator;

internal RestControllerRequestHandler()
{
_restMethodCollection = ImmutableArray<RestControllerMethodInfo>.Empty;
_responseFactory = new RestResponseFactory();
_methodExecuteFactory = new RestControllerMethodExecutorFactory();
_uriParser = new UriParser();
_restControllerMethodInfoValidator = new RestControllerMethodInfoValidator();
}

internal void RegisterController<T>() where T : class
Expand All @@ -41,11 +43,15 @@ internal void RegisterController<T>(Func<object[]> constructorArgs) where T : cl

private void AddRestMethods<T>(IEnumerable<RestControllerMethodInfo> restControllerMethodInfos) where T : class
{
_restMethodCollection = _restMethodCollection.Concat(restControllerMethodInfos)
var newControllerMethodInfos = restControllerMethodInfos.ToArray();

_restControllerMethodInfoValidator.Validate<T>(_restMethodCollection, newControllerMethodInfos);

_restMethodCollection = _restMethodCollection.Concat(newControllerMethodInfos)
.OrderByDescending(x => x.MethodInfo.GetParameters().Count())
.ToImmutableArray();

InstanceCreatorCache.Default.CacheCreator(typeof (T));
InstanceCreatorCache.Default.CacheCreator(typeof(T));
}

internal IEnumerable<RestControllerMethodInfo> GetRestMethods<T>(Func<object[]> constructorArgs) where T : class
Expand Down
45 changes: 45 additions & 0 deletions src/WebServer/Rest/UniqueMatchUriAndVerbComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Linq;
using Restup.WebServer;

namespace Restup.Webserver.Rest
{
internal class UniqueMatchUriAndVerbRestControllerMethodInfoComparer : IEqualityComparer<RestControllerMethodInfo>
{
private readonly PathPartsAndParametersParsedUriComparer _parsedUriComparer;

public UniqueMatchUriAndVerbRestControllerMethodInfoComparer()
{
_parsedUriComparer = new PathPartsAndParametersParsedUriComparer();
}

public bool Equals(RestControllerMethodInfo x, RestControllerMethodInfo y)
{
return _parsedUriComparer.Equals(x.MatchUri, y.MatchUri) && x.Verb == y.Verb;
}

public int GetHashCode(RestControllerMethodInfo obj)
{
unchecked
{
return ((obj.MatchUri?.GetHashCode() ?? 0) * Constants.HashCodePrime) ^ (int)obj.Verb;
}
}

internal class PathPartsAndParametersParsedUriComparer : IEqualityComparer<ParsedUri>
{
public bool Equals(ParsedUri x, ParsedUri y)
{
return x.PathParts.SequenceEqual(y.PathParts) && x.Parameters.SequenceEqual(y.Parameters);
}

public int GetHashCode(ParsedUri obj)
{
unchecked
{
return ((obj.Parameters?.GetHashCode() ?? 0) * Constants.HashCodePrime) ^ (obj.PathParts?.GetHashCode() ?? 0);
}
}
}
}
}
25 changes: 25 additions & 0 deletions src/WebServer/Rest/UriParameter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System;
using Restup.WebServer;

namespace Restup.Webserver.Rest
{
internal class UriParameter
Expand All @@ -15,5 +18,27 @@ public UriParameter(string name, string value)
Name = name;
Value = value;
}

protected bool Equals(UriParameter other)
{
return string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) &&
string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
}

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((UriParameter) obj);
}

public override int GetHashCode()
{
unchecked
{
return ((Name?.GetHashCode() ?? 0) * Constants.HashCodePrime) ^ (Value?.GetHashCode() ?? 0);
}
}
}
}
Loading

0 comments on commit 9079ffe

Please sign in to comment.