public Task <ITriggerBinding> TryCreateAsync(TriggerBindingProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }

            ParameterInfo        parameter = context.Parameter;
            HttpTriggerAttribute attribute = parameter.GetCustomAttribute <HttpTriggerAttribute>(inherit: false);

            if (attribute == null)
            {
                return(Task.FromResult <ITriggerBinding>(null));
            }

            // Can bind to user types, HttpRequestMessage, object (for dynamic binding support) and all the Read
            // Types supported by StreamValueBinder
            IEnumerable <Type> supportedTypes = StreamValueBinder.GetSupportedTypes(FileAccess.Read)
                                                .Union(new Type[] { typeof(HttpRequest), typeof(object), typeof(HttpRequestMessage) });
            bool isSupportedTypeBinding = ValueBinder.MatchParameterType(parameter, supportedTypes);
            bool isUserTypeBinding      = !isSupportedTypeBinding && IsValidUserType(parameter.ParameterType);

            if (!isSupportedTypeBinding && !isUserTypeBinding)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                                                                  "Can't bind HttpTriggerAttribute to type '{0}'.", parameter.ParameterType));
            }

            return(Task.FromResult <ITriggerBinding>(new HttpTriggerBinding(attribute, context.Parameter, isUserTypeBinding, _responseHook)));
        }
Beispiel #2
0
        public void ValidateFunction_ThrowsOnDuplicateName()
        {
            var httpFunctions = new Dictionary <string, HttpTriggerAttribute>();
            var name          = "test";

            // first add an http function
            var metadata  = new FunctionMetadata();
            var function  = new Mock <FunctionDescriptor>(MockBehavior.Strict, name, null, metadata, null, null, null, null);
            var attribute = new HttpTriggerAttribute(AuthorizationLevel.Function, "get");

            function.Setup(p => p.GetTriggerAttributeOrNull <HttpTriggerAttribute>()).Returns(() => attribute);

            ScriptHost.ValidateFunction(function.Object, httpFunctions);

            // add a proxy with same name
            metadata = new FunctionMetadata()
            {
                IsProxy = true
            };
            function  = new Mock <FunctionDescriptor>(MockBehavior.Strict, name, null, metadata, null, null, null, null);
            attribute = new HttpTriggerAttribute(AuthorizationLevel.Function, "get")
            {
                Route = "proxyRoute"
            };
            function.Setup(p => p.GetTriggerAttributeOrNull <HttpTriggerAttribute>()).Returns(() => attribute);

            var ex = Assert.Throws <InvalidOperationException>(() =>
            {
                ScriptHost.ValidateFunction(function.Object, httpFunctions);
            });

            Assert.Equal(string.Format($"The function or proxy name '{name}' must be unique within the function app.", name), ex.Message);
        }
Beispiel #3
0
        public void Constructor_AuthLevelOnly_ReturnsExpectedResult()
        {
            var attrib = new HttpTriggerAttribute(AuthorizationLevel.Admin);

            Assert.Equal(AuthorizationLevel.Admin, attrib.AuthLevel);
            Assert.Null(attrib.Methods);
        }
        public Task <ITriggerBinding> TryCreateAsync(TriggerBindingProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }

            ParameterInfo        parameter = context.Parameter;
            HttpTriggerAttribute attribute = parameter.GetCustomAttribute <HttpTriggerAttribute>(inherit: false);

            if (attribute == null)
            {
                return(Task.FromResult <ITriggerBinding>(null));
            }

            bool isSupportedTypeBinding = ValueBinder.MatchParameterType(parameter, _supportedTypes);
            bool isUserTypeBinding      = !isSupportedTypeBinding && IsValidUserType(parameter.ParameterType);

            if (!isSupportedTypeBinding && !isUserTypeBinding)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                                                                  "Can't bind HttpTriggerAttribute to type '{0}'.", parameter.ParameterType));
            }

            return(Task.FromResult <ITriggerBinding>(new HttpTriggerBinding(attribute, context.Parameter, isUserTypeBinding, _responseHook)));
        }
Beispiel #5
0
        public void HttpRoutesConflict_ReturnsExpectedResult()
        {
            var first = new HttpTriggerAttribute
            {
                Route = "foo/bar/baz"
            };
            var second = new HttpTriggerAttribute
            {
                Route = "foo/bar"
            };

            Assert.False(ScriptHost.HttpRoutesConflict(first, second));
            Assert.False(ScriptHost.HttpRoutesConflict(second, first));

            first = new HttpTriggerAttribute
            {
                Route = "foo/bar/baz"
            };
            second = new HttpTriggerAttribute
            {
                Route = "foo/bar/baz"
            };
            Assert.True(ScriptHost.HttpRoutesConflict(first, second));
            Assert.True(ScriptHost.HttpRoutesConflict(second, first));

            // no conflict since methods do not intersect
            first = new HttpTriggerAttribute(AuthorizationLevel.Function, "get", "head")
            {
                Route = "foo/bar/baz"
            };
            second = new HttpTriggerAttribute(AuthorizationLevel.Function, "post", "put")
            {
                Route = "foo/bar/baz"
            };
            Assert.False(ScriptHost.HttpRoutesConflict(first, second));
            Assert.False(ScriptHost.HttpRoutesConflict(second, first));

            first = new HttpTriggerAttribute(AuthorizationLevel.Function, "get", "head")
            {
                Route = "foo/bar/baz"
            };
            second = new HttpTriggerAttribute
            {
                Route = "foo/bar/baz"
            };
            Assert.True(ScriptHost.HttpRoutesConflict(first, second));
            Assert.True(ScriptHost.HttpRoutesConflict(second, first));

            first = new HttpTriggerAttribute(AuthorizationLevel.Function, "get", "head", "put", "post")
            {
                Route = "foo/bar/baz"
            };
            second = new HttpTriggerAttribute(AuthorizationLevel.Function, "put")
            {
                Route = "foo/bar/baz"
            };
            Assert.True(ScriptHost.HttpRoutesConflict(first, second));
            Assert.True(ScriptHost.HttpRoutesConflict(second, first));
        }
        /// <inheritdoc />
        public OperationType GetHttpVerb(HttpTriggerAttribute trigger)
        {
            var verb = Enum.TryParse <OperationType>(trigger.Methods.First(), true, out OperationType ot)
                           ? ot
                           : throw new InvalidOperationException();

            return(verb);
        }
 public static string GetRouteOrDefault(this HttpTriggerAttribute trigger, string defaultRoute = "/")
 {
     if (string.IsNullOrWhiteSpace(defaultRoute))
     {
         throw new ArgumentException("Value cannot be null or whitespace.", nameof(defaultRoute));
     }
     return(trigger.Route?.AsPath() ?? defaultRoute.AsPath());
 }
Beispiel #8
0
 protected static void MakeParameterString(HttpTriggerAttribute trigger, List <OpenApiParameter> parameters, HttpMethod httpMethod, string route, ParameterLocation location, string name)
 {
     FixParameter(trigger, parameters, httpMethod, route, location, name, (parameter) =>
     {
         parameter.Schema = new OpenApiSchema {
             Type = "string", Format = "string"
         };
     });
 }
Beispiel #9
0
        public void Constructor_MethodsOnly_ReturnsExpectedResult()
        {
            var attrib = new HttpTriggerAttribute("GET", "POST");

            Assert.Equal(AuthorizationLevel.Function, attrib.AuthLevel);
            Assert.Equal(2, attrib.Methods.Length);
            Assert.Equal("GET", attrib.Methods[0]);
            Assert.Equal("POST", attrib.Methods[1]);
        }
Beispiel #10
0
        //https://github.com/Azure/azure-functions-vs-build-sdk/issues/16
        public void HttpTriggerShouldHaveStringForAuthLevelEnum()
        {
            var attribute = new HttpTriggerAttribute(AuthorizationLevel.Function);

            var jObject = attribute.ToJObject();

            jObject.Should().HaveElement("authLevel");
            jObject["authLevel"].Should().Be("function");
        }
Beispiel #11
0
        public void WithRouteSpecified_ReturnsRouteAsPath()
        {
            var trigger = new HttpTriggerAttribute("get")
            {
                Route = "/mypath"
            };

            Assert.Equal("/mypath", trigger.GetRouteOrDefault());
        }
Beispiel #12
0
        protected static void FixParameter(HttpTriggerAttribute trigger, List <OpenApiParameter> parameters, HttpMethod httpMethod, string route, ParameterLocation location, string name, Action <OpenApiParameter> fix)
        {
            if (trigger.Route != route || !trigger.Methods.Contains(httpMethod.ToString()))
            {
                return;
            }

            var parameter = parameters.First(p => p.In == location && p.Name == name);

            fix(parameter);
        }
        public void HttpTriggerAttributeWithWebHookTypeShouldntHaveAnAuthLevel()
        {
            var attribute = new HttpTriggerAttribute()
            {
                WebHookType = "something"
            };

            var jObject = attribute.ToJObject();

            jObject["authLevel"].Should().BeNull();
        }
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FunctionAuthorizationRequirement requirement, FunctionDescriptor resource)
        {
            HttpTriggerAttribute httpTrigger = resource.GetTriggerAttributeOrNull <HttpTriggerAttribute>();

            if (httpTrigger != null && PrincipalHasAuthLevelClaim(context.User, httpTrigger.AuthLevel))
            {
                context.Succeed(requirement);
            }

            return(Task.CompletedTask);
        }
        /// <inheritdoc />
        public List <OpenApiParameter> GetOpenApiParameters(MethodInfo element, HttpTriggerAttribute trigger)
        {
            var parameters = element.GetCustomAttributes <OpenApiParameterAttribute>(inherit: false)
                             .Select(p => p.ToOpenApiParameter())
                             .ToList();

            if (trigger.AuthLevel != AuthorizationLevel.Anonymous)
            {
                parameters.AddOpenApiParameter <string>("code", @in: ParameterLocation.Query, required: false);
            }

            return(parameters);
        }
        /// <inheritdoc />
        public List <OpenApiParameter> GetOpenApiParameters(MethodInfo element, HttpTriggerAttribute trigger, NamingStrategy namingStrategy = null)
        {
            var parameters = element.GetCustomAttributes <OpenApiParameterAttribute>(inherit: false)
                             .Select(p => p.ToOpenApiParameter(namingStrategy))
                             .ToList();

            // This needs to be provided separately.
            if (trigger.AuthLevel != AuthorizationLevel.Anonymous)
            {
                parameters.AddOpenApiParameter <string>("code", @in: ParameterLocation.Query, required: false);
            }

            return(parameters);
        }
Beispiel #17
0
        AuthorizationLevel GetAuthorizationLevel(OperationFilterContext Context)
        {
            foreach (ParameterInfo Parameter in Context.MethodInfo.GetParameters())
            {
                HttpTriggerAttribute Attribute = Parameter.GetCustomAttribute <HttpTriggerAttribute>();

                if (Attribute != null)
                {
                    return(Attribute.AuthLevel);
                }
            }

            throw new Exception();
        }
            /// <summary>
            /// Gets the static strongly typed binding data contract
            /// </summary>
            internal Dictionary <string, Type> GetBindingDataContract(HttpTriggerAttribute attribute, ParameterInfo parameter)
            {
                // add contract members for POCO binding properties if binding to a POCO
                var aggregateDataContract = new Dictionary <string, Type>(StringComparer.OrdinalIgnoreCase);

                if (_isUserTypeBinding && _bindingDataProvider?.Contract != null)
                {
                    aggregateDataContract.AddRange(_bindingDataProvider.Contract);
                }

                // Mark that HttpTrigger accepts a return value.
                aggregateDataContract["$return"] = typeof(object).MakeByRefType();

                // add contract members for any route parameters
                if (!string.IsNullOrEmpty(attribute.Route))
                {
                    var routeParameters = TemplateParser.Parse(attribute.Route).Parameters;
                    var parameters      = ((MethodInfo)parameter.Member).GetParameters().ToDictionary(p => p.Name, p => p.ParameterType, StringComparer.OrdinalIgnoreCase);
                    foreach (TemplatePart routeParameter in routeParameters)
                    {
                        // don't override if the contract already includes a name
                        if (!aggregateDataContract.ContainsKey(routeParameter.Name))
                        {
                            // if there is a method parameter mapped to this parameter
                            // derive the Type from that
                            Type type;
                            if (!parameters.TryGetValue(routeParameter.Name, out type))
                            {
                                type = typeof(string);
                            }
                            aggregateDataContract[routeParameter.Name] = type;
                        }
                    }
                }

                // add headers collection to the contract
                if (!aggregateDataContract.ContainsKey(HttpHeadersKey))
                {
                    aggregateDataContract.Add(HttpHeadersKey, typeof(IDictionary <string, string>));
                }

                // add query parameter collection to binding contract
                if (!aggregateDataContract.ContainsKey(HttpQueryKey))
                {
                    aggregateDataContract.Add(HttpQueryKey, typeof(IDictionary <string, string>));
                }

                return(aggregateDataContract);
            }
Beispiel #19
0
        protected static void FixEnumParameter <TEnum>(HttpTriggerAttribute trigger, List <OpenApiParameter> parameters, HttpMethod httpMethod, string route, ParameterLocation location, string name)
            where TEnum : struct, IConvertible
        {
            if (!typeof(TEnum).IsEnum)
            {
                throw new ArgumentException("TEnum must be an enumerated type");
            }

            FixParameter(trigger, parameters, httpMethod, route, location, name, (parameter) =>
            {
                parameter.Schema = new OpenApiSchema {
                    Enum = GetEnumValues <TEnum>(), Type = "string", Format = "string"
                };
            });
        }
Beispiel #20
0
        private HttpTriggerAttribute FindHttpTriggerAttribute(MethodInfo methodInfo)
        {
            HttpTriggerAttribute triggerAttribute = null;

            foreach (var parameter in methodInfo.GetParameters())
            {
                triggerAttribute = parameter.GetCustomAttributes(typeof(HttpTriggerAttribute), false)
                                   .FirstOrDefault() as HttpTriggerAttribute;
                if (triggerAttribute != null)
                {
                    break;
                }
            }

            return(triggerAttribute);
        }
        /// <inheritdoc />
        public List <OpenApiParameter> GetOpenApiParameters(MethodInfo element, HttpTriggerAttribute trigger, NamingStrategy namingStrategy, VisitorCollection collection)
        {
            var parameters = element.GetCustomAttributes <OpenApiParameterAttribute>(inherit: false)
                             .Where(p => p.Deprecated == false)
                             .Select(p => p.ToOpenApiParameter(namingStrategy, collection))
                             .ToList();

            // // TODO: Should this be forcibly provided?
            // // This needs to be provided separately.
            // if (trigger.AuthLevel != AuthorizationLevel.Anonymous)
            // {
            //     parameters.AddOpenApiParameter<string>("code", @in: ParameterLocation.Query, required: false);
            // }

            return(parameters);
        }
        internal static void ValidateHttpFunction(string functionName, HttpTriggerAttribute httpTrigger, bool isProxy = false)
        {
            if (string.IsNullOrWhiteSpace(httpTrigger.Route) && !isProxy)
            {
                // if no explicit route is provided, default to the function name
                httpTrigger.Route = functionName;
            }

            // disallow custom routes in our own reserved route space
            string httpRoute = httpTrigger.Route.Trim('/').ToLowerInvariant();

            if (httpRoute.StartsWith("admin"))
            {
                throw new InvalidOperationException("The specified route conflicts with one or more built in routes.");
            }
        }
            public HttpTriggerBinding(HttpTriggerAttribute attribute, ParameterInfo parameter, bool isUserTypeBinding, Action <HttpRequest, object> responseHook = null)
            {
                _responseHook      = responseHook;
                _parameter         = parameter;
                _isUserTypeBinding = isUserTypeBinding;

                if (_isUserTypeBinding)
                {
                    // Create the BindingDataProvider from the user Type. The BindingDataProvider
                    // is used to define the binding parameters that the binding exposes to other
                    // bindings (i.e. the properties of the POCO can be bound to by other bindings).
                    // It is also used to extract the binding data from an instance of the Type.
                    _bindingDataProvider = BindingDataProvider.FromType(parameter.ParameterType);
                }

                _bindingDataContract = GetBindingDataContract(attribute, parameter);
            }
Beispiel #24
0
        private bool TryGetHttpTrigger(MethodInfo methodInfo, out HttpTriggerAttribute triggerAttribute)
        {
            triggerAttribute = null;
            var ignore = methodInfo.GetCustomAttributes().Any(x => x is SwaggerIgnoreAttribute);

            if (ignore)
            {
                return(false);
            }

            triggerAttribute = FindHttpTriggerAttribute(methodInfo);
            if (triggerAttribute == null)
            {
                return(false);
            }

            return(true);
        }
        // A route is in conflict if the route matches any other existing
        // route and there is intersection in the http methods of the two functions
        internal static bool HttpRoutesConflict(HttpTriggerAttribute httpTrigger, HttpTriggerAttribute otherHttpTrigger)
        {
            if (string.Compare(httpTrigger.Route.Trim('/'), otherHttpTrigger.Route.Trim('/'), StringComparison.OrdinalIgnoreCase) != 0)
            {
                // routes differ, so no conflict
                return(false);
            }

            if (httpTrigger.Methods == null || httpTrigger.Methods.Length == 0 ||
                otherHttpTrigger.Methods == null || otherHttpTrigger.Methods.Length == 0)
            {
                // if either methods collection is null or empty that means
                // "all methods", which will intersect with any method collection
                return(true);
            }

            return(httpTrigger.Methods.Intersect(otherHttpTrigger.Methods).Any());
        }
        private static dynamic GeneratePaths(Assembly assembly, dynamic doc)
        {
            dynamic paths   = new ExpandoObject();
            var     methods = assembly.GetTypes()
                              .SelectMany(t => t.GetMethods())
                              .Where(m => m.GetCustomAttributes(typeof(FunctionNameAttribute), false).Length > 0)
                              .ToArray();

            foreach (MethodInfo methodInfo in methods)
            {
                string route = "/api/";

                var functionAttr = (FunctionNameAttribute)methodInfo.GetCustomAttributes(typeof(FunctionNameAttribute), false)
                                   .Single();

                if (functionAttr.Name == SwaggerFunctionName)
                {
                    continue;
                }

                HttpTriggerAttribute triggerAttribute = null;
                foreach (ParameterInfo parameter in methodInfo.GetParameters())
                {
                    triggerAttribute = parameter.GetCustomAttributes(typeof(HttpTriggerAttribute), false)
                                       .FirstOrDefault() as HttpTriggerAttribute;
                    if (triggerAttribute != null)
                    {
                        break;
                    }
                }
                if (triggerAttribute == null)
                {
                    continue;                           // Trigger attribute is required in an Azure function
                }
                if (!string.IsNullOrWhiteSpace(triggerAttribute.Route))
                {
                    route += triggerAttribute.Route;
                }
                else
                {
                    route += functionAttr.Name;
                }

                dynamic path = new ExpandoObject();

                var verbs = triggerAttribute.Methods ?? new[] { "get", "post", "delete", "head", "patch", "put", "options" };
                foreach (string verb in verbs)
                {
                    dynamic operation = new ExpandoObject();
                    operation.operationId = ToTitleCase(functionAttr.Name) + ToTitleCase(verb);
                    operation.produces    = new[] { "application/json" };
                    operation.consumes    = new[] { "application/json" };
                    operation.parameters  = GenerateFunctionParametersSignature(methodInfo, route, doc);

                    // Summary is title
                    operation.summary = GetFunctionName(methodInfo, functionAttr.Name);
                    // Verbose description
                    operation.description = GetFunctionDescription(methodInfo, functionAttr.Name);

                    operation.responses = GenerateResponseParameterSignature(methodInfo, doc);
                    dynamic keyQuery = new ExpandoObject();
                    keyQuery.apikeyQuery = new string[0];
                    operation.security   = new ExpandoObject[] { keyQuery };

                    // Microsoft Flow import doesn't like two apiKey options, so we leave one out.
                    //dynamic apikeyHeader = new ExpandoObject();
                    //apikeyHeader.apikeyHeader = new string[0];
                    //operation.security = new ExpandoObject[] { keyQuery, apikeyHeader };

                    AddToExpando(path, verb, operation);
                }
                AddToExpando(paths, route, path);
            }
            return(paths);
        }
 private static IList <string> GetMethods(HttpTriggerAttribute httpTriggerAttribute)
 {
     return((httpTriggerAttribute.Methods ?? _defaultMethods)
            .Select(m => m.Trim().ToUpperInvariant())
            .ToList());
 }
        /// <inheritdoc />
        public string GetHttpEndpoint(FunctionNameAttribute function, HttpTriggerAttribute trigger)
        {
            var endpoint = $"/{(string.IsNullOrWhiteSpace(trigger.Route) ? function.Name : this.FilterRoute(trigger.Route)).Trim('/')}";

            return(endpoint);
        }
Beispiel #29
0
        private dynamic GeneratePaths(Assembly assembly, dynamic doc, string apiTitle, string apiDefinitionName, string pathPrefix)
        {
            dynamic paths   = new ExpandoObject();
            var     methods = assembly.GetTypes()
                              .SelectMany(t => t.GetMethods())
                              .Where(m => m.GetCustomAttributes(typeof(FunctionNameAttribute), false).Length > 0).ToArray();

            foreach (MethodInfo methodInfo in methods)
            {
                //hide any disabled methods
                if (methodInfo.GetCustomAttributes(typeof(DisableAttribute), true).Any() ||
                    methodInfo.GetCustomAttributes(typeof(SwaggerIgnoreAttribute), true).Any())
                {
                    continue;
                }

                var route = pathPrefix;

                var functionAttr = (FunctionNameAttribute)methodInfo.GetCustomAttributes(typeof(FunctionNameAttribute), false)
                                   .Single();

                if (functionAttr.Name == apiDefinitionName)
                {
                    continue;
                }

                HttpTriggerAttribute triggerAttribute = null;
                foreach (ParameterInfo parameter in methodInfo.GetParameters())
                {
                    triggerAttribute = parameter.GetCustomAttributes(typeof(HttpTriggerAttribute), false)
                                       .FirstOrDefault() as HttpTriggerAttribute;
                    if (triggerAttribute != null)
                    {
                        break;
                    }
                }
                if (triggerAttribute == null)
                {
                    continue;                           // Trigger attribute is required in an Azure function
                }
                if (!string.IsNullOrWhiteSpace(triggerAttribute.Route))
                {
                    route += triggerAttribute.Route;
                }
                else
                {
                    route += functionAttr.Name;
                }

                dynamic path = new ExpandoObject();

                var verbs = triggerAttribute.Methods ?? new[] { "get", "post", "delete", "head", "patch", "put", "options" };
                foreach (string verb in verbs)
                {
                    dynamic operation = new ExpandoObject();
                    operation.operationId = ToTitleCase(functionAttr.Name);
                    operation.produces    = new[] { "application/json" };
                    operation.consumes    = new[] { "application/json" };
                    operation.parameters  = GenerateFunctionParametersSignature(methodInfo, route, doc);

                    // Summary is title
                    operation.summary = GetFunctionName(methodInfo, functionAttr.Name);
                    // Verbose description
                    operation.description = GetFunctionDescription(methodInfo, functionAttr.Name);

                    operation.responses = GenerateResponseParameterSignature(methodInfo, doc);
                    operation.tags      = new[] { apiTitle };

                    dynamic keyQuery = new ExpandoObject();
                    keyQuery.apikeyQuery = new string[0];
                    operation.security   = new ExpandoObject[] { keyQuery };

                    AddToExpando(path, verb, operation);
                }
                AddToExpando(paths, route, path);
            }
            return(paths);
        }
Beispiel #30
0
        public void ValidateFunction_ValidatesHttpRoutes()
        {
            var httpFunctions = new Dictionary <string, HttpTriggerAttribute>();

            // first add an http function
            var metadata  = new FunctionMetadata();
            var function  = new Mock <FunctionDescriptor>(MockBehavior.Strict, "test", null, metadata, null, null, null, null);
            var attribute = new HttpTriggerAttribute(AuthorizationLevel.Function, "get")
            {
                Route = "products/{category}/{id?}"
            };

            function.Setup(p => p.GetTriggerAttributeOrNull <HttpTriggerAttribute>()).Returns(() => attribute);

            ScriptHost.ValidateFunction(function.Object, httpFunctions);
            Assert.Equal(1, httpFunctions.Count);
            Assert.True(httpFunctions.ContainsKey("test"));

            // add another for a completely different route
            function  = new Mock <FunctionDescriptor>(MockBehavior.Strict, "test2", null, metadata, null, null, null, null);
            attribute = new HttpTriggerAttribute(AuthorizationLevel.Function, "get")
            {
                Route = "/foo/bar/baz/"
            };
            function.Setup(p => p.GetTriggerAttributeOrNull <HttpTriggerAttribute>()).Returns(() => attribute);
            ScriptHost.ValidateFunction(function.Object, httpFunctions);
            Assert.Equal(2, httpFunctions.Count);
            Assert.True(httpFunctions.ContainsKey("test2"));

            // add another that varies from another only by http methods
            function  = new Mock <FunctionDescriptor>(MockBehavior.Strict, "test3", null, metadata, null, null, null, null);
            attribute = new HttpTriggerAttribute(AuthorizationLevel.Function, "put", "post")
            {
                Route = "/foo/bar/baz/"
            };
            function.Setup(p => p.GetTriggerAttributeOrNull <HttpTriggerAttribute>()).Returns(() => attribute);
            ScriptHost.ValidateFunction(function.Object, httpFunctions);
            Assert.Equal(3, httpFunctions.Count);
            Assert.True(httpFunctions.ContainsKey("test3"));

            // now try to add a function for the same route
            // where the http methods overlap
            function  = new Mock <FunctionDescriptor>(MockBehavior.Strict, "test4", null, metadata, null, null, null, null);
            attribute = new HttpTriggerAttribute
            {
                Route = "/foo/bar/baz/"
            };
            function.Setup(p => p.GetTriggerAttributeOrNull <HttpTriggerAttribute>()).Returns(() => attribute);
            var ex = Assert.Throws <InvalidOperationException>(() =>
            {
                ScriptHost.ValidateFunction(function.Object, httpFunctions);
            });

            Assert.Equal("The route specified conflicts with the route defined by function 'test2'.", ex.Message);
            Assert.Equal(3, httpFunctions.Count);

            // try to add a route under reserved admin route
            function  = new Mock <FunctionDescriptor>(MockBehavior.Strict, "test5", null, metadata, null, null, null, null);
            attribute = new HttpTriggerAttribute
            {
                Route = "admin/foo/bar"
            };
            function.Setup(p => p.GetTriggerAttributeOrNull <HttpTriggerAttribute>()).Returns(() => attribute);
            ex = Assert.Throws <InvalidOperationException>(() =>
            {
                ScriptHost.ValidateFunction(function.Object, httpFunctions);
            });
            Assert.Equal("The specified route conflicts with one or more built in routes.", ex.Message);

            // verify that empty route is defaulted to function name
            function  = new Mock <FunctionDescriptor>(MockBehavior.Strict, "test6", null, metadata, null, null, null, null);
            attribute = new HttpTriggerAttribute();
            function.Setup(p => p.GetTriggerAttributeOrNull <HttpTriggerAttribute>()).Returns(() => attribute);
            ScriptHost.ValidateFunction(function.Object, httpFunctions);
            Assert.Equal(4, httpFunctions.Count);
            Assert.True(httpFunctions.ContainsKey("test6"));
            Assert.Equal("test6", attribute.Route);
        }