feanz
1/9/2015 - 4:53 PM

RouteTestingExtensions WebAPI Only

RouteTestingExtensions WebAPI Only

  /// <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;
        }
    }