Created
January 9, 2015 16:53
-
-
Save feanz/83f0f5942adda76424ba to your computer and use it in GitHub Desktop.
RouteTestingExtensions WebAPI Only
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// Used to simplify testing routes and restful testing routes | |
/// <example> | |
/// This tests that incoming PUT on resource is handled by the update method of the Banner controller | |
/// "~/banner/1" | |
/// .WithMethod(HttpVerbs.Put) | |
/// .ShouldMapTo<BannerController>(action => action.Update(1)); | |
/// </example> | |
/// </summary> | |
public static class RouteTestingExtensions | |
{ | |
/// <summary> | |
/// Will return the name of the action specified in the ActionNameAttribute for a method if it has an | |
/// ActionNameAttribute. | |
/// Will return the name of the method otherwise. | |
/// </summary> | |
public static string ActionName(this MethodInfo method) | |
{ | |
return method.IsDecoratedWith<ActionNameAttribute>() ? method.GetAttribute<ActionNameAttribute>().Name : method.Name; | |
} | |
/// <summary> | |
/// Will return true if the attributeTarget is decorated with an attribute of type TAttribute. Or false if not | |
/// </summary> | |
public static bool IsDecoratedWith<TAttribute>(this ICustomAttributeProvider attributeTarget) where TAttribute : Attribute | |
{ | |
return attributeTarget.GetCustomAttributes(typeof(TAttribute), false).Length > 0; | |
} | |
/// <summary> | |
/// Will return the first attribute of type TAttribute on the attributeTarget. | |
/// </summary> | |
public static TAttribute GetAttribute<TAttribute>(this ICustomAttributeProvider attributeTarget) where TAttribute : Attribute | |
{ | |
return (TAttribute)attributeTarget.GetCustomAttributes(typeof(TAttribute), false)[0]; | |
} | |
/// <summary> | |
/// Returns the corresponding http route for the URL. Returns null if no route was found. | |
/// </summary> | |
/// <param name="url">The app relative url to test.</param> | |
/// <param name="config"></param> | |
/// <returns>A matching <see cref="RouteData" />, or null.</returns> | |
public static HttpRouteTester Route(this string url, HttpConfiguration config) | |
{ | |
return Route(url, config, HttpMethod.Get); | |
} | |
/// <summary> | |
/// Returns the corresponding http route for the URL. Returns null if no route was found. | |
/// </summary> | |
/// <param name="url">The app relative url to test.</param> | |
/// <param name="config"></param> | |
/// <param name="httpMethod">HttpMethod</param> | |
/// <returns>A matching <see cref="RouteData" />, or null.</returns> | |
public static HttpRouteTester Route(this string url, HttpConfiguration config, HttpMethod httpMethod) | |
{ | |
var httpRequestMessage = new HttpRequestMessage(httpMethod, url); | |
return new HttpRouteTester(config, httpRequestMessage); | |
} | |
/// <summary> | |
/// Converts the URL to matching web api route and verifies that it will match a route with the values specified by the | |
/// expression. | |
/// </summary> | |
/// <typeparam name="TController">The type of web api controller</typeparam> | |
/// <param name="url">The ~/ based url</param> | |
/// <param name="config"></param> | |
/// <param name="action">The expression that defines what action gets called (and with which parameters)</param> | |
public static HttpRouteTester ShouldMapTo<TController>(this string url, HttpConfiguration config, Expression<Func<TController, HttpResponseMessage>> action) where TController : ApiController | |
{ | |
url = ConvertRelativeUrlToAbsolute(url); | |
return url.Route(config).ShouldMapTo(action); | |
} | |
/// <summary> | |
/// Converts the URL to matching web api route and verifies that it will match a route with the values specified by the | |
/// expression. | |
/// </summary> | |
/// <typeparam name="TController">The type of web api controller</typeparam> | |
/// <param name="url">The ~/ based url</param> | |
/// <param name="config"></param> | |
/// <param name="action">The expression that defines what action gets called (and with which parameters)</param> | |
public static HttpRouteTester ShouldMapTo<TController>(this string url, HttpConfiguration config, Expression<Func<TController, Task<HttpResponseMessage>>> action) | |
where TController : ApiController | |
{ | |
url = ConvertRelativeUrlToAbsolute(url); | |
return url.Route(config).ShouldMapTo(action); | |
} | |
private static string ConvertRelativeUrlToAbsolute(string url) | |
{ | |
return url.StartsWith("~") ? url.Replace("~", "http://www.site.com") : url; | |
} | |
/// <summary> | |
/// Verifies the <see cref="RouteData">routeData</see> maps to the controller type specified. | |
/// </summary> | |
/// <typeparam name="TController"></typeparam> | |
/// <param name="routeTester"></param> | |
/// <returns></returns> | |
public static HttpRouteTester ShouldMapTo<TController>(this HttpRouteTester routeTester) where TController : ApiController | |
{ | |
var expecting = typeof(TController); | |
if (expecting != routeTester.GetControllerType()) | |
throw new RouteTestingException(string.Format("Controller types do not match expecting:{0} actual:{1}", expecting, routeTester.GetControllerType())); | |
return routeTester; | |
} | |
/// <summary> | |
/// Asserts that the route matches the expression specified. Checks controller, action, and any method arguments | |
/// into the action as route values. | |
/// </summary> | |
/// <typeparam name="TController">The controller.</typeparam> | |
/// <param name="routeTester">The routeData to check</param> | |
/// <param name="action">The action to call on TController.</param> | |
public static HttpRouteTester ShouldMapTo<TController>(this HttpRouteTester routeTester, Expression<Func<TController, HttpResponseMessage>> action) | |
where TController : ApiController | |
{ | |
if (routeTester == null) | |
throw new ArgumentException("The URL did not match any route"); | |
routeTester.ShouldMapTo<TController>(); | |
CheckActionMaps(routeTester, action); | |
return routeTester; | |
} | |
/// <summary> | |
/// Asserts that the route matches the expression specified. Checks controller, action, and any method arguments | |
/// into the action as route values. | |
/// </summary> | |
/// <typeparam name="TController">The controller.</typeparam> | |
/// <param name="routeTester">The routeData to check</param> | |
/// <param name="action">The action to call on TController.</param> | |
public static HttpRouteTester ShouldMapTo<TController>(this HttpRouteTester routeTester, Expression<Func<TController, Task<HttpResponseMessage>>> action) | |
where TController : ApiController | |
{ | |
if (routeTester == null) | |
throw new ArgumentException("The URL did not match any route"); | |
routeTester.ShouldMapTo<TController>(); | |
CheckActionMaps(routeTester, action); | |
return routeTester; | |
} | |
private static void CheckActionMaps<TController>(HttpRouteTester routeTester, Expression<Func<TController, Task<HttpResponseMessage>>> action) where TController : ApiController | |
{ | |
var methodCall = (MethodCallExpression)action.Body; | |
var actualAction = routeTester.GetActionName(); | |
var expectedAction = methodCall.Method.ActionName(); | |
if (expectedAction != actualAction) | |
throw new RouteTestingException(string.Format("Action did not match expecting:{0} actual:{1}", expectedAction, actualAction)); | |
} | |
private static void CheckActionMaps<TController>(HttpRouteTester routeTester, Expression<Func<TController, HttpResponseMessage>> action) where TController : ApiController | |
{ | |
var methodCall = (MethodCallExpression)action.Body; | |
var actualAction = routeTester.GetActionName(); | |
var expectedAction = methodCall.Method.ActionName(); | |
if (expectedAction != actualAction) | |
throw new RouteTestingException(string.Format("Action did not match expecting:{0} actual:{1}", expectedAction, actualAction)); | |
} | |
/// <summary> | |
/// A way to start the fluent interface and and which method to use | |
/// since you have a method constraint in the route. | |
/// </summary> | |
/// <param name="url"></param> | |
/// <param name="config"></param> | |
/// <param name="httpMethod"></param> | |
/// <returns></returns> | |
public static HttpRouteTester WithMethod(this string url, HttpConfiguration config, HttpMethod httpMethod) | |
{ | |
return Route(url, config, httpMethod); | |
} | |
} | |
public class RouteTestingException : Exception | |
{ | |
public RouteTestingException(string message) | |
: base(message) | |
{ | |
} | |
public RouteTestingException(string format, params object[] args) | |
: base(string.Format(format, args)) | |
{ | |
} | |
} | |
public class HttpRouteTester | |
{ | |
private readonly HttpControllerContext _controllerContext; | |
private readonly IHttpControllerSelector _controllerSelector; | |
private readonly HttpRequestMessage _request; | |
public HttpRouteTester(HttpConfiguration config, HttpRequestMessage request) | |
{ | |
if (config.Routes.Count == 0) | |
throw new ArgumentException("No routes found in route table make sure you register routes before testing route matching"); | |
_request = request; | |
RouteData = config.Routes.GetRouteData(request); | |
if (RouteData == null) | |
throw new ArgumentException("No route data found for this route"); | |
request.Properties[HttpPropertyKeys.HttpRouteDataKey] = RouteData; | |
_controllerSelector = new DefaultHttpControllerSelector(config); | |
_controllerContext = new HttpControllerContext(config, RouteData, request); | |
} | |
public IHttpRouteData RouteData { get; private set; } | |
public string GetActionName() | |
{ | |
if (_controllerContext.ControllerDescriptor == null) | |
GetControllerType(); | |
var actionSelector = new ApiControllerActionSelector(); | |
var descriptor = actionSelector.SelectAction(_controllerContext); | |
return descriptor.ActionName; | |
} | |
public Type GetControllerType() | |
{ | |
var descriptor = _controllerSelector.SelectController(_request); | |
_controllerContext.ControllerDescriptor = descriptor; | |
return descriptor.ControllerType; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment