VinnyG
VinnyG

Reputation: 6911

How to unit test a route using custom model binder

I have a custom model binder that checks if user has access to the document he asked for; I want to know how I can test the route that uses this custom binder?

I try with this test but I get this error :

MvcContrib.TestHelper.AssertionException : Value for parameter 'contract' did not match: expected 'Domain.Models.Contract' but was ''; no value found in the route context action parameter named 'contract' - does your matching route contain a token called 'contract'?

[SetUp]
public void Setup()
{
    MvcApplication.RegisterModelBinders();
    MvcApplication.RegisterRoutes(RouteTable.Routes);
}

[Test]
public void VersionEdit()
{
    var contract = TestHelper.CreateContract();
    var route = "~/Contract/" + contract.Id.ToString() + "/Version/Edit/" + 
        contract.Versions.Count;
    route.ShouldMapTo<VersionController>(c => c.Edit(contract));
}

If I try to debug the custom binder never gets called.

My route definition :

public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "VersionToken", // Route name
                "Contract/{contractId}/Version/{version}/{action}/{token}", // URL with parameters
                new { controller = "Version", action = "ViewContract", version = 1, token = UrlParameter.Optional } // Parameter defaults
            );

            routes.MapRoute(
                "Version", // Route name
                "Contract/{contractId}/Version/{version}/{action}", // URL with parameters
                new { controller = "Version", action = "Create", version = UrlParameter.Optional } // Parameter defaults
            );

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

            if (HttpContext.Current != null && !HttpContext.Current.IsDebuggingEnabled) routes.IgnoreRoute("CI");
        }

My Model binder :

public static void RegisterModelBinders()
        {
            var session = (ISession)DependencyResolver.Current.GetService(typeof(ISession));
            var authService = (IAuthenticationService)DependencyResolver.Current.GetService(typeof(IAuthenticationService));
            System.Web.Mvc.ModelBinders.Binders[typeof (Contract)] = new ContractModelBinder(session, authService);
        }

public class ContractModelBinder : DefaultModelBinder
    {
        private readonly ISession _session;
        private readonly IAuthenticationService _authService;
        public ContractModelBinder(ISession session, IAuthenticationService authService)
        {
            _session = session;
            _authService = authService;
        }

        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var contractId = GetValue(bindingContext, "contractId");
            var version = GetA<int>(bindingContext,"version");
            var token = GetValue(bindingContext, "token");

            var contract = _session.Single<Contract>(contractId);
            if (contract == null) 
            {
                throw new HttpException(404, "Not found");
            }
            if (contract.Versions.Count < version.Value)
            {
                throw new HttpException(404, "Not found");
            }
            contract.RequestedVersionNumber = version.Value;
            if(token == null)
            {
                var user = _authService.LoggedUser();
                if (user == null) throw new HttpException(401, "Unauthorized");
                if (contract.CreatedBy == null || !contract.CreatedBy.Id.HasValue || contract.CreatedBy.Id.Value != user.Id)
                {
                    throw new HttpException(403, "Forbidden");
                }
            }
            else
            {
                contract.RequestedToken = token;
                var userToken = contract.RequestedVersion.Tokens.SingleOrDefault(x => x.Token == token);
                if (userToken == null)
                {
                    throw new HttpException(401, "Unauthorized");
                }
            }

            return contract;
        }

        private static T? GetA<T>(ModelBindingContext bindingContext, string key) where T : struct, IComparable
        {
            if (String.IsNullOrEmpty(key)) return null;
            //Try it with the prefix...
            var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + "." + key);
            //Didn't work? Try without the prefix if needed...
            if (valueResult == null && bindingContext.FallbackToEmptyPrefix)
            {
                valueResult = bindingContext.ValueProvider.GetValue(key);
            }
            if (valueResult == null)
            {
                return null;
            }
            return (T)valueResult.ConvertTo(typeof(T));
        }

        private static string GetValue(ModelBindingContext bindingContext, string key)
        {
            if (String.IsNullOrEmpty(key)) return null;
            //Try it with the prefix...
            var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + "." + key);
            //Didn't work? Try without the prefix if needed...
            if (valueResult == null && bindingContext.FallbackToEmptyPrefix)
            {
                valueResult = bindingContext.ValueProvider.GetValue(key);
            }
            if (valueResult == null)
            {
                return null;
            }
            return valueResult.AttemptedValue;
        }
    }

Upvotes: 4

Views: 1429

Answers (1)

Darin Dimitrov
Darin Dimitrov

Reputation: 1038720

When testing routes the MvcContrib TestHelper doesn't invoke the MVC pipeline and the model binder. The model binder should be unit tested separately. Binding is separate from routing and occurs once the controller is instantiated and the action invoked.

In this example you are unit testing routes. So all you need to ensure is that ~/Contract/5/Version/Edit/3 correctly maps to the Edit action on your VersionController which is done like this:

"~/Contract/5/Version/Edit/3".ShouldMapTo<VersionController>(c => c.Edit(null));

Upvotes: 5

Related Questions