From 4a7d0c8980e4c337069371210df6809ba4b7e8f1 Mon Sep 17 00:00:00 2001 From: "Eric Sibly [chullybun]" Date: Sat, 7 Dec 2024 14:09:09 -0800 Subject: [PATCH] v5.1.0 (#80) - *Enhancement:* Where an `HttpRequest` is used for an Azure Functions `HttpTriggerTester` the passed `HttpRequest.PathAndQuery` is checked against that defined by the corresponding `HttpTriggerAttribute.Route` and will result in an error where different. The `HttpTrigger.WithRouteChecK` and `WithNoRouteCheck` methods control the path and query checking as needed. --- CHANGELOG.md | 3 + Common.targets | 2 +- README.md | 12 + .../Azure/Functions/FunctionTesterBase.cs | 2 +- .../Azure/Functions/HttpTriggerTester.cs | 233 +++++++++++++++++- .../Azure/Functions/RouteCheckOption.cs | 38 +++ .../Functions/ServiceBusTriggerTester.cs | 8 +- .../ExtensionMethods.cs | 69 ++++++ .../FunctionTester.cs | 2 +- src/UnitTestEx/Hosting/HostTesterBase.cs | 29 ++- tests/UnitTestEx.Function/PersonFunction.cs | 8 + .../PersonFunctionTest.cs | 7 + .../ProductFunctionTest.cs | 8 +- .../IsolatedFunctionTest.cs | 2 +- .../PersonFunctionTest.cs | 32 ++- .../ProductFunctionTest.cs | 6 +- .../PersonFunctionTest.cs | 8 + .../ProductFunctionTest.cs | 8 +- 18 files changed, 441 insertions(+), 36 deletions(-) create mode 100644 src/UnitTestEx.Azure.Functions/Azure/Functions/RouteCheckOption.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 985e0b8..f5db89b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Represents the **NuGet** versions. +## v5.1.0 +- *Enhancement:* Where an `HttpRequest` is used for an Azure Functions `HttpTriggerTester` the passed `HttpRequest.PathAndQuery` is checked against that defined by the corresponding `HttpTriggerAttribute.Route` and will result in an error where different. The `HttpTrigger.WithRouteChecK` and `WithNoRouteCheck` methods control the path and query checking as needed. + ## v5.0.0 - *Enhancement:* `UnitTestEx` package updated to include only standard .NET core capabilities; new packages created to house specific as follows: - `UnitTestEx.Azure.Functions` created to house Azure Functions specific capabilities; diff --git a/Common.targets b/Common.targets index fd0bb24..06e2ecd 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 5.0.0 + 5.1.0 preview Avanade Avanade diff --git a/README.md b/README.md index 5b9273c..5292f1d 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ test.ReplaceHttpClientFactory(mcf) Both the [_Isolated worker model_](https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide) and [_In-process model_](https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library) are supported. +Additionally, where an `HttpRequest` is used the passed `HttpRequest.PathAndQuery` is checked against that defined by the corresponding `HttpTriggerAttribute.Route` and will result in an error where different. The `HttpTrigger.WithRouteChecK` and `WithNoRouteCheck` methods control the path and query checking as needed. +
## Service Bus-trigger Azure Function @@ -113,6 +115,14 @@ test.Run(gin => gin.Pour())
+## DI Mocking + +Each of the aforementioned test capabilities support Dependency Injection (DI) mocking. This is achieved by replacing the registered services with mocks, stubs, or fakes. The [`TesterBase`](./src/UnitTestEx/Abstractions/TesterBaseT.cs) enables using the `Mock*`, `Replace*` and `ConfigureServices` methods. + +The underlying `Services` property also provides access to the `IServiceCollection` within the underlying test host to enable further configuration as required. + +
+ ## HTTP Client mocking Where invoking a down-stream system using an [`HttpClient`](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) within a unit test context this should generally be mocked. To enable _UnitTestEx_ provides a [`MockHttpClientFactory`](./src/UnitTestEx/Mocking/MockHttpClientFactory.cs) to manage each `HttpClient` (one or more), and mock a response based on the configured request. This leverages the [Moq](https://github.com/moq/moq4) framework internally to enable. One or more requests can also be configured per `HttpClient`. @@ -132,6 +142,8 @@ test.ReplaceHttpClientFactory(mcf) .Assert(new { id = "Abc", description = "A blue carrot" }); ``` +The `ReplaceHttpClientFactory` leverages the `Replace*` capabilities discussed earlier in [DI Mocking](#di-mocking). +
### HTTP Client configurations diff --git a/src/UnitTestEx.Azure.Functions/Azure/Functions/FunctionTesterBase.cs b/src/UnitTestEx.Azure.Functions/Azure/Functions/FunctionTesterBase.cs index 381a989..38094ed 100644 --- a/src/UnitTestEx.Azure.Functions/Azure/Functions/FunctionTesterBase.cs +++ b/src/UnitTestEx.Azure.Functions/Azure/Functions/FunctionTesterBase.cs @@ -320,7 +320,7 @@ public HttpRequest CreateHttpRequest(HttpMethod httpMethod, string? requestUri = var context = new DefaultHttpContext(); - var uri = new Uri(requestUri!, UriKind.RelativeOrAbsolute); + var uri = requestUri is null ? new Uri("http://functiontest") : new Uri(requestUri, UriKind.RelativeOrAbsolute); if (!uri.IsAbsoluteUri) uri = new Uri($"http://functiontest{(requestUri != null && requestUri.StartsWith('/') ? requestUri : $"/{requestUri}")}"); diff --git a/src/UnitTestEx.Azure.Functions/Azure/Functions/HttpTriggerTester.cs b/src/UnitTestEx.Azure.Functions/Azure/Functions/HttpTriggerTester.cs index 863af4f..2a0035e 100644 --- a/src/UnitTestEx.Azure.Functions/Azure/Functions/HttpTriggerTester.cs +++ b/src/UnitTestEx.Azure.Functions/Azure/Functions/HttpTriggerTester.cs @@ -27,20 +27,77 @@ namespace UnitTestEx.Azure.Functions /// Provides Azure Function or unit-testing capabilities. /// /// The Azure Function . + /// To aid with the testing the following checks are automatically performed during execution with an thrown when: + /// + /// The or does not contain the . + /// This check can be further configured using the and methods prior to the execution. + /// The or does not match the and combination. + /// This check can be further configured using the and methods prior to the execution. + /// + /// The above checks are generally neccessary to assist in ensuring that the function is being invoked correctly given the parameters have to be explicitly passed in separately. + /// public class HttpTriggerTester : HostTesterBase, IExpectations> where TFunction : class { + private bool _methodCheck = true; + private RouteCheckOption _routeCheckOption = RouteCheckOption.PathAndQuery; + private StringComparison _routeComparison = StringComparison.OrdinalIgnoreCase; + /// /// Initializes a new class. /// /// The owning . /// The . - public HttpTriggerTester(TesterBase owner, IServiceScope serviceScope) : base(owner, serviceScope) => ExpectationsArranger = new ExpectationsArranger>(owner, this); + public HttpTriggerTester(TesterBase owner, IServiceScope serviceScope) : base(owner, serviceScope) + { + ExpectationsArranger = new ExpectationsArranger>(owner, this); + this.SetHttpMethodCheck(owner.SetUp); + this.SetHttpRouteCheck(owner.SetUp); + } /// /// Gets the . /// public ExpectationsArranger> ExpectationsArranger { get; } + /// + /// Indicates that no check is performed to ensure that the or contain the . + /// + /// Defaults to configuration values where configured; otherwise, . + public HttpTriggerTester WithNoMethodCheck() + { + _methodCheck = false; + return this; + } + + /// + /// Indicates that a check is performed to ensure that the or contain the . + /// + /// Defaults to configuration values where configured; otherwise, this is the default. + public HttpTriggerTester WithMethodCheck() + { + _methodCheck = true; + return this; + } + + /// + /// Sets the to be . + /// + /// Defaults to configuration values where configured; otherwise, the default is and + public HttpTriggerTester WithNoRouteCheck() => WithRouteCheck(RouteCheckOption.None); + + /// + /// Sets the to be checked during execution. + /// + /// The . + /// The . + /// Defaults to configuration values where configured; otherwise, the default is and + public HttpTriggerTester WithRouteCheck(RouteCheckOption option, StringComparison? comparison = StringComparison.OrdinalIgnoreCase) + { + _routeCheckOption = option; + _routeComparison = comparison ?? StringComparison.OrdinalIgnoreCase; + return this; + } + /// /// Runs the HTTP Triggered (see or ) function using an within the . /// @@ -61,13 +118,26 @@ public async Task RunAsync(Expression $"'{x.ToUpperInvariant()}'"))}; however, invoked using '{httpRequest.Method.ToUpperInvariant()}' which is not valid."); + if (httpRequest is not null) + { + var httpTriggerAttribute = a as Microsoft.Azure.WebJobs.HttpTriggerAttribute; + if (httpTriggerAttribute is not null) + { + if (_methodCheck && !httpTriggerAttribute.Methods.Contains(httpRequest.Method, StringComparer.OrdinalIgnoreCase)) + throw new InvalidOperationException($"The function {nameof(Microsoft.Azure.WebJobs.HttpTriggerAttribute)} supports {nameof(Microsoft.Azure.WebJobs.HttpTriggerAttribute.Methods)} of {string.Join(" or ", httpTriggerAttribute.Methods.Select(x => $"'{x.ToUpperInvariant()}'"))}; however, invoked using '{httpRequest.Method.ToUpperInvariant()}' which is not valid. Use '{nameof(WithNoMethodCheck)}' to change this behavior."); + + CheckRoute(httpRequest, httpTriggerAttribute.Route, p); + } + + var httpTriggerAttribute2 = a as Microsoft.Azure.Functions.Worker.HttpTriggerAttribute; + if (httpTriggerAttribute2 is not null) + { + if (_methodCheck && httpTriggerAttribute2.Methods is not null && !httpTriggerAttribute2.Methods!.Contains(httpRequest.Method, StringComparer.OrdinalIgnoreCase)) + throw new InvalidOperationException($"The function {nameof(Microsoft.Azure.Functions.Worker.HttpTriggerAttribute)} supports {nameof(Microsoft.Azure.Functions.Worker.HttpTriggerAttribute.Methods)} of {string.Join(" or ", httpTriggerAttribute2.Methods.Select(x => $"'{x.ToUpperInvariant()}'"))}; however, invoked using '{httpRequest.Method.ToUpperInvariant()}' which is not valid. Use '{nameof(WithNoMethodCheck)}' to change this behavior."); - var httpTriggerAttribute2 = a as Microsoft.Azure.Functions.Worker.HttpTriggerAttribute; - if (httpRequest != null && httpTriggerAttribute2 is not null && httpTriggerAttribute2.Methods is not null && !httpTriggerAttribute2.Methods!.Contains(httpRequest.Method, StringComparer.OrdinalIgnoreCase)) - throw new InvalidOperationException($"The function {nameof(Microsoft.Azure.Functions.Worker.HttpTriggerAttribute)} supports {nameof(Microsoft.Azure.Functions.Worker.HttpTriggerAttribute.Methods)} of {string.Join(" or ", httpTriggerAttribute2.Methods.Select(x => $"'{x.ToUpperInvariant()}'"))}; however, invoked using '{httpRequest.Method.ToUpperInvariant()}' which is not valid."); + CheckRoute(httpRequest, httpTriggerAttribute2.Route, p); + } + } }).ConfigureAwait(false); await Task.Delay(UnitTestEx.TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); @@ -79,6 +149,155 @@ public async Task RunAsync(Expression + /// Format replacement inspired by: https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs + /// + private static string FormatReplacement(string route, (string? Name, object? Value)[] @params) + { + var sb = new StringBuilder(); + var scanIndex = 0; + var endIndex = route.Length; + + while (scanIndex < endIndex) + { + var openBraceIndex = FindBraceIndex(route, '{', scanIndex, endIndex); + if (scanIndex == 0 && openBraceIndex == endIndex) + return route; // No holes found. + + var closeBraceIndex = FindBraceIndex(route, '}', openBraceIndex, endIndex); + if (closeBraceIndex == endIndex) + { + sb.Append(route, scanIndex, endIndex - scanIndex); + scanIndex = endIndex; + } + else + { + sb.Append(route, scanIndex, openBraceIndex - scanIndex); + + if (@params is not null) + { + var pval = @params.Where(x => x.Name != null && MemoryExtensions.Equals(route.AsSpan(openBraceIndex + 1, closeBraceIndex - openBraceIndex - 1), x.Name, StringComparison.Ordinal)).Select(x => x.Value).FirstOrDefault(); + if (pval is not null) + sb.Append(FormatValue(pval)); + } + + scanIndex = closeBraceIndex + 1; + } + } + + return sb.ToString(); + } + + /// + /// Find the brace index within specified range. + /// + private static int FindBraceIndex(string format, char brace, int startIndex, int endIndex) + { + // Example: {{prefix{{{Argument}}}suffix}}. + var braceIndex = endIndex; + var scanIndex = startIndex; + var braceOccurenceCount = 0; + + while (scanIndex < endIndex) + { + if (braceOccurenceCount > 0 && format[scanIndex] != brace) + { + if (braceOccurenceCount % 2 == 0) + { + // Even number of '{' or '}' found. Proceed search with next occurence of '{' or '}'. + braceOccurenceCount = 0; + braceIndex = endIndex; + } + else + { + // An unescaped '{' or '}' found. + break; + } + } + else if (format[scanIndex] == brace) + { + if (brace == '}') + { + if (braceOccurenceCount == 0) + { + // For '}' pick the first occurence. + braceIndex = scanIndex; + } + } + else + { + // For '{' pick the last occurence. + braceIndex = scanIndex; + } + + braceOccurenceCount++; + } + + scanIndex++; + } + + return braceIndex; + } + + /// + /// Formats the value. + /// + private static string FormatValue(object value) + { + if (value is DateTime dt) + return dt.ToString("o", CultureInfo.InvariantCulture); + else if (value is DateTimeOffset dto) + return dto.ToString("o", CultureInfo.InvariantCulture); + else if (value is bool b) + return b.ToString().ToLowerInvariant(); + else if (value is IFormattable fmt) + return fmt.ToString(null, CultureInfo.InvariantCulture); + else + return value.ToString() ?? string.Empty; + } + /// /// Log the request to the output. /// diff --git a/src/UnitTestEx.Azure.Functions/Azure/Functions/RouteCheckOption.cs b/src/UnitTestEx.Azure.Functions/Azure/Functions/RouteCheckOption.cs new file mode 100644 index 0000000..acacbe4 --- /dev/null +++ b/src/UnitTestEx.Azure.Functions/Azure/Functions/RouteCheckOption.cs @@ -0,0 +1,38 @@ +namespace UnitTestEx.Azure.Functions +{ + /// + /// Represents the route check option for the . + /// + public enum RouteCheckOption + { + /// + /// No route check is required, + /// + None, + + /// + /// The route should match the specified path excluding any query string. + /// + Path, + + /// + /// The route should match the specified path including the query string. + /// + PathAndQuery, + + /// + /// The route should start with the specified path and query string. + /// + PathAndQueryStartsWith, + + /// + /// The route query (ignore path) should match the specified query string. + /// + Query, + + /// + /// The route query (ignore path) should start with the specified query string. + /// + QueryStartsWith + } +} \ No newline at end of file diff --git a/src/UnitTestEx.Azure.Functions/Azure/Functions/ServiceBusTriggerTester.cs b/src/UnitTestEx.Azure.Functions/Azure/Functions/ServiceBusTriggerTester.cs index cf2adbc..ae4cdca 100644 --- a/src/UnitTestEx.Azure.Functions/Azure/Functions/ServiceBusTriggerTester.cs +++ b/src/UnitTestEx.Azure.Functions/Azure/Functions/ServiceBusTriggerTester.cs @@ -69,13 +69,13 @@ public async Task RunAsync(Expression> expre sbv = v; foreach (var pi in p) { - if (pi is ServiceBusReceivedMessage sbrm) + if (pi.Value is ServiceBusReceivedMessage sbrm) sbv = sbrm; - else if (pi is WebJobsServiceBusMessageActionsAssertor psba) + else if (pi.Value is WebJobsServiceBusMessageActionsAssertor psba) sba = psba; - else if (pi is WebJobsServiceBusSessionMessageActionsAssertor pssba) + else if (pi.Value is WebJobsServiceBusSessionMessageActionsAssertor pssba) ssba = pssba; - else if (pi is WorkerServiceBusMessageActionsAssertor pwsba) + else if (pi.Value is WorkerServiceBusMessageActionsAssertor pwsba) wsba = pwsba; } diff --git a/src/UnitTestEx.Azure.Functions/ExtensionMethods.cs b/src/UnitTestEx.Azure.Functions/ExtensionMethods.cs index 77d7304..68ebf7d 100644 --- a/src/UnitTestEx.Azure.Functions/ExtensionMethods.cs +++ b/src/UnitTestEx.Azure.Functions/ExtensionMethods.cs @@ -1,5 +1,6 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx +using Microsoft.AspNetCore.Http; using Microsoft.Azure.WebJobs.ServiceBus; using System; using UnitTestEx.Abstractions; @@ -12,6 +13,10 @@ namespace UnitTestEx /// public static class ExtensionMethods { + internal const string HttpMethodCheckName = "HttpTriggerTester_MethodCheck"; + internal const string HttpRouteCheckOptionName = "HttpTriggerTester_" + nameof(RouteCheckOption); + internal const string HttpRouteComparisonName = "HttpTriggerTester_" + nameof(StringComparison); + /// /// Creates a as the instance to enable test mock and assert verification. /// @@ -34,5 +39,69 @@ public static class ExtensionMethods /// The . /// The . public static WorkerServiceBusMessageActionsAssertor CreateWorkerServiceBusMessageActions(this TesterBase tester) => new(tester.Implementor); + + /// + /// Sets the default that no check is performed to ensure that the or contains the for the . + /// + /// The . + public static TestSetUp WithNoMethodCheck(this TestSetUp setup) + { + setup.Properties[HttpMethodCheckName] = false; + return setup; + } + + /// + /// Sets the default that a check is performed to ensure that the or contains the for the . + /// + /// The . + public static TestSetUp WithMethodCheck(this TestSetUp setup) + { + setup.Properties[HttpMethodCheckName] = true; + return setup; + } + + /// + /// Sets the default to be for the . + /// + /// The . + public static TestSetUp WithNoHttpRouteCheck(this TestSetUp setup) => WithHttpRouteCheck(setup, RouteCheckOption.None); + + /// + /// Sets the default to be checked during execution for the . + /// + /// The . + /// The . + /// The . + public static TestSetUp WithHttpRouteCheck(this TestSetUp setup, RouteCheckOption option, StringComparison? comparison = StringComparison.OrdinalIgnoreCase) + { + setup.Properties[HttpRouteCheckOptionName] = option; + setup.Properties[HttpRouteComparisonName] = comparison; + return setup; + } + + /// + /// Invokes the or method based on the property. + /// + /// The Azure Function . + /// The . + /// The . + internal static void SetHttpMethodCheck(this HttpTriggerTester tester, TestSetUp setup) where TFunction : class + { + if (!setup.Properties.TryGetValue(HttpMethodCheckName, out var check) || (bool)check! == true) + tester.WithMethodCheck(); + else + tester.WithNoMethodCheck(); + } + + /// + /// Invokes the method to set the and from the . + /// + /// The Azure Function . + /// The . + /// The . + internal static void SetHttpRouteCheck(this HttpTriggerTester tester, TestSetUp setup) where TFunction : class + => tester.WithRouteCheck( + setup.Properties.TryGetValue(HttpRouteCheckOptionName, out var option) ? (RouteCheckOption)option! : RouteCheckOption.PathAndQuery, + setup.Properties.TryGetValue(HttpRouteComparisonName, out var comparison) ? (StringComparison)comparison! : StringComparison.OrdinalIgnoreCase); } } \ No newline at end of file diff --git a/src/UnitTestEx.Azure.Functions/FunctionTester.cs b/src/UnitTestEx.Azure.Functions/FunctionTester.cs index b93668a..15818af 100644 --- a/src/UnitTestEx.Azure.Functions/FunctionTester.cs +++ b/src/UnitTestEx.Azure.Functions/FunctionTester.cs @@ -7,7 +7,7 @@ namespace UnitTestEx { /// - /// Provides the NUnit Function testing capability. + /// Provides the Function testing capability. /// public static class FunctionTester { diff --git a/src/UnitTestEx/Hosting/HostTesterBase.cs b/src/UnitTestEx/Hosting/HostTesterBase.cs index f4b717f..f00af6c 100644 --- a/src/UnitTestEx/Hosting/HostTesterBase.cs +++ b/src/UnitTestEx/Hosting/HostTesterBase.cs @@ -10,6 +10,8 @@ using UnitTestEx.Abstractions; using UnitTestEx.Json; +using ParameterNameValuePair = (string? Name, object? Value); + namespace UnitTestEx.Hosting { /// @@ -52,13 +54,13 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) /// The optional parameter (s) to find. /// Action to verify the method parameters prior to method invocation. /// The resulting exception if any and elapsed milliseconds. - protected async Task<(Exception? Exception, double ElapsedMilliseconds)> RunAsync(Expression> expression, Type[]? paramAttributeTypes, Action? onBeforeRun) + protected async Task<(Exception? Exception, double ElapsedMilliseconds)> RunAsync(Expression> expression, Type[]? paramAttributeTypes, OnBeforeRun? onBeforeRun) { TestSetUp.LogAutoSetUpOutputs(Implementor); var mce = MethodCallExpressionValidate(expression); var pis = mce.Method.GetParameters(); - var @params = new object?[pis.Length]; + var @params = new ParameterNameValuePair[pis.Length]; Attribute? paramAttribute = null; object? paramValue = null; @@ -66,7 +68,7 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) { var ue = Expression.Convert(mce.Arguments[i], typeof(object)); var le = Expression.Lambda>(ue); - @params[i] = le.Compile().Invoke(); + @params[i] = new(pis[i].Name, le.Compile().Invoke()); if (paramAttribute == null && paramAttributeTypes != null) { @@ -77,7 +79,7 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) break; } - paramValue = @params[i]; + paramValue = @params[i].Value; } } @@ -88,7 +90,8 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) try { - await ((Task)mce.Method.Invoke(h, @params)!).ConfigureAwait(false); + var p = @params.Select(x => x.Value).ToArray(); + await ((Task)mce.Method.Invoke(h, p)!).ConfigureAwait(false); sw.Stop(); return (null, sw.Elapsed.TotalMilliseconds); } @@ -112,13 +115,13 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) /// The optional parameter array to find. /// Action to verify the method parameters prior to method invocation. /// The resulting value, resulting exception if any, and elapsed milliseconds. - protected async Task<(TValue Result, Exception? Exception, double ElapsedMilliseconds)> RunAsync(Expression>> expression, Type[]? paramAttributeTypes, Action? onBeforeRun) + protected async Task<(TValue Result, Exception? Exception, double ElapsedMilliseconds)> RunAsync(Expression>> expression, Type[]? paramAttributeTypes, OnBeforeRun? onBeforeRun) { TestSetUp.LogAutoSetUpOutputs(Implementor); var mce = MethodCallExpressionValidate(expression); var pis = mce.Method.GetParameters(); - var @params = new object?[pis.Length]; + var @params = new ParameterNameValuePair[pis.Length]; Attribute? paramAttribute = null; object? paramValue = null; @@ -126,7 +129,7 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) { var ue = Expression.Convert(mce.Arguments[i], typeof(object)); var le = Expression.Lambda>(ue); - @params[i] = le.Compile().Invoke(); + @params[i] = new(pis[i].Name, le.Compile().Invoke()); if (paramAttribute == null && paramAttributeTypes != null) { @@ -137,7 +140,7 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) break; } - paramValue = @params[i]; + paramValue = @params[i].Value; } } @@ -164,6 +167,14 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) } } + /// + /// Represents the on before run delegate. + /// + /// The parameter name and value array (all method parameters). + /// The selected parameter found. + /// The corresponding value for the parameter with the . + protected delegate void OnBeforeRun(ParameterNameValuePair[] parameters, Attribute? paramAttribute, object? paramValue); + /// /// Validates that the is a valid . /// diff --git a/tests/UnitTestEx.Function/PersonFunction.cs b/tests/UnitTestEx.Function/PersonFunction.cs index ff12088..f45005e 100644 --- a/tests/UnitTestEx.Function/PersonFunction.cs +++ b/tests/UnitTestEx.Function/PersonFunction.cs @@ -69,6 +69,14 @@ public async Task RunWithContent([HttpTrigger(AuthorizationLevel. log.LogInformation("C# HTTP trigger function processed a request."); return new ContentResult { Content = JsonSerializer.Serialize(new { first = person.FirstName, last = person.LastName }), ContentType = MediaTypeNames.Application.Json, StatusCode = 200 }; } + + [FunctionName("PersonFunctionQuery")] + public async Task RunWithQuery([HttpTrigger(AuthorizationLevel.Function, "get", Route = "api/people?name={name}")] HttpRequest request, string name, ILogger log) + { + await Task.CompletedTask.ConfigureAwait(false); + log.LogInformation("C# HTTP trigger function processed a request."); + return new OkObjectResult(new { name }); + } } public class Namer diff --git a/tests/UnitTestEx.MSTest.Test/PersonFunctionTest.cs b/tests/UnitTestEx.MSTest.Test/PersonFunctionTest.cs index e211873..dbd422b 100644 --- a/tests/UnitTestEx.MSTest.Test/PersonFunctionTest.cs +++ b/tests/UnitTestEx.MSTest.Test/PersonFunctionTest.cs @@ -13,6 +13,7 @@ public async Task NoData() { using var test = FunctionTester.Create(); (await test.HttpTrigger() + .WithNoRouteCheck() .RunAsync(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person"), test.Logger))) .AssertOK() .AssertValue("This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."); @@ -23,6 +24,7 @@ public void QueryString() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person?name=Trevor"), test.Logger)) .AssertOK() .AssertValue("Hello, Trevor. This HTTP triggered function executed successfully."); @@ -33,6 +35,7 @@ public void WithBody() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Get, "person", new { name = "Jane" }), test.Logger)) .AssertOK() .AssertValue("Hello, Jane. This HTTP triggered function executed successfully."); @@ -43,6 +46,7 @@ public void BadRequest1() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Post, "person", new { name = "Brian" }), test.Logger)) .AssertBadRequest() .AssertErrors("Name cannot be Brian."); @@ -53,6 +57,7 @@ public void BadRequest2() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Post, "person", new { name = "Brian" }), test.Logger)) .AssertBadRequest() .AssertErrors(new ApiError("name", "Name cannot be Brian.")); @@ -63,6 +68,7 @@ public void ValidJson() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Get, "person", new { name = "Rachel" }), test.Logger)) .AssertOK() .AssertValue(new { FirstName = "Rachel", LastName = "Smith" }); @@ -73,6 +79,7 @@ public void ValidJsonResource() { using var test = FunctionTester.Create().UseJsonSerializer(new Json.JsonSerializer()); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Get, "person", new { name = "Rachel" }), test.Logger)) .AssertOK() .AssertValueFromJsonResource("FunctionTest-ValidJsonResource.json"); diff --git a/tests/UnitTestEx.MSTest.Test/ProductFunctionTest.cs b/tests/UnitTestEx.MSTest.Test/ProductFunctionTest.cs index 8385429..5f6db81 100644 --- a/tests/UnitTestEx.MSTest.Test/ProductFunctionTest.cs +++ b/tests/UnitTestEx.MSTest.Test/ProductFunctionTest.cs @@ -19,7 +19,7 @@ public void Notfound() using var test = FunctionTester.Create(); test.ReplaceHttpClientFactory(mcf) .HttpTrigger() - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/xyz"), "xyz", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/xyz"), "xyz", test.Logger)) .AssertNotFound(); } @@ -33,7 +33,7 @@ public void Success() using var test = FunctionTester.Create(); test.ReplaceHttpClientFactory(mcf) .HttpTrigger() - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/abc"), "abc", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc", test.Logger)) .AssertOK() .AssertValue(new { id = "Abc", description = "A blue carrot" }); } @@ -48,7 +48,7 @@ public void Success2() using var test = FunctionTester.Create(); test.ReplaceHttpClientFactory(mcf) .Type() - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/abc"), "abc", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc", test.Logger)) .ToActionResultAssertor() .AssertOK() .AssertValue(new { id = "Abc", description = "A blue carrot" }); @@ -62,7 +62,7 @@ public void Exception() using var test = FunctionTester.Create(); test.ReplaceHttpClientFactory(mcf) .HttpTrigger() - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/exception"), "exception", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/exception"), "exception", test.Logger)) .AssertException("An unexpected exception occured."); } } diff --git a/tests/UnitTestEx.NUnit.Test/IsolatedFunctionTest.cs b/tests/UnitTestEx.NUnit.Test/IsolatedFunctionTest.cs index 5099416..684d962 100644 --- a/tests/UnitTestEx.NUnit.Test/IsolatedFunctionTest.cs +++ b/tests/UnitTestEx.NUnit.Test/IsolatedFunctionTest.cs @@ -14,7 +14,7 @@ public void HttpTrigger() using var test = FunctionTester.Create(); test.HttpTrigger() .ExpectLogContains("C# HTTP trigger function processed a request.") - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "hello"))) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, null))) .AssertSuccess(); } } diff --git a/tests/UnitTestEx.NUnit.Test/PersonFunctionTest.cs b/tests/UnitTestEx.NUnit.Test/PersonFunctionTest.cs index 721bda6..0ba1e72 100644 --- a/tests/UnitTestEx.NUnit.Test/PersonFunctionTest.cs +++ b/tests/UnitTestEx.NUnit.Test/PersonFunctionTest.cs @@ -13,6 +13,7 @@ public async Task NoData() { using var test = FunctionTester.Create(); (await test.HttpTrigger() + .WithNoRouteCheck() .RunAsync(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person"), test.Logger))) .AssertOK() .AssertValue("This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."); @@ -23,6 +24,7 @@ public void QueryString() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person?name=Trevor"), test.Logger)) .AssertOK() .AssertValue("Hello, Trevor. This HTTP triggered function executed successfully."); @@ -33,6 +35,7 @@ public void WithBody() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Get, "person", new { name = "Jane" }), test.Logger)) .AssertOK() .AssertValue("Hello, Jane. This HTTP triggered function executed successfully."); @@ -43,7 +46,9 @@ public void BadRequest1() { using var test = FunctionTester.Create(); test.HttpTrigger() - .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Post, "person", new { name = "Brian" }), test.Logger)) + .WithNoMethodCheck() + .WithNoRouteCheck() + .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Delete, "person", new { name = "Brian" }), test.Logger)) .AssertBadRequest() .AssertErrors("Name cannot be Brian."); } @@ -53,6 +58,7 @@ public void BadRequest2() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Post, "person", new { name = "Brian" }), test.Logger)) .AssertBadRequest() .AssertErrors(new ApiError("name", "Name cannot be Brian.")); @@ -63,6 +69,7 @@ public void ValidJson() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Get, "person", new { name = "Rachel" }), test.Logger)) .AssertOK() .AssertValue(new { FirstName = "Rachel", LastName = "Smith" }); @@ -73,6 +80,7 @@ public void ValidJsonResource() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Get, "person", new { name = "Rachel" }), test.Logger)) .AssertOK() .AssertValueFromJsonResource("FunctionTest-ValidJsonResource.json"); @@ -97,5 +105,27 @@ public void ValueVsHttpRequestContent() .AssertOK() .AssertValue(new { first = "Rachel", last = "Smith" }); } + + [Test] + public void WithPathAndQueryCheck() + { + using var test = FunctionTester.Create(); + test.HttpTrigger() + .WithRouteCheck(Azure.Functions.RouteCheckOption.Query) + .Run(f => f.RunWithQuery(test.CreateHttpRequest(HttpMethod.Get, "https://blah/api/persons?name=Damien"), "Damien", test.Logger)) + .AssertOK() + .AssertValue(new { name = "Damien" }); + } + + [Test] + public void WithPathAndQueryStartsWithCheck() + { + using var test = FunctionTester.Create(); + test.HttpTrigger() + .WithRouteCheck(Azure.Functions.RouteCheckOption.QueryStartsWith) + .Run(f => f.RunWithQuery(test.CreateHttpRequest(HttpMethod.Get, "https://blah/api/persons?name=Damien&$order=name"), "Damien", test.Logger)) + .AssertOK() + .AssertValue(new { name = "Damien" }); + } } } \ No newline at end of file diff --git a/tests/UnitTestEx.NUnit.Test/ProductFunctionTest.cs b/tests/UnitTestEx.NUnit.Test/ProductFunctionTest.cs index 55cc92a..2e03094 100644 --- a/tests/UnitTestEx.NUnit.Test/ProductFunctionTest.cs +++ b/tests/UnitTestEx.NUnit.Test/ProductFunctionTest.cs @@ -23,7 +23,7 @@ public void Notfound() using var test = FunctionTester.Create(); test.ReplaceHttpClientFactory(mcf) .HttpTrigger() - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/xyz"), "xyz", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/xyz"), "xyz", test.Logger)) .AssertNotFound(); } @@ -38,7 +38,7 @@ public void Success() test.ReplaceHttpClientFactory(mcf) .HttpTrigger() .ExpectLogContains("C# HTTP trigger function processed a request.") - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/abc"), "abc", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc", test.Logger)) .AssertOK() .AssertValue(new { id = "Abc", description = "A blue carrot" }); } @@ -67,7 +67,7 @@ public void Exception() using var test = FunctionTester.Create(); test.ReplaceHttpClientFactory(mcf) .HttpTrigger() - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/exception"), "exception", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/exception"), "exception", test.Logger)) .AssertException("An unexpected exception occured."); } diff --git a/tests/UnitTestEx.Xunit.Test/PersonFunctionTest.cs b/tests/UnitTestEx.Xunit.Test/PersonFunctionTest.cs index 40b2161..92eb39e 100644 --- a/tests/UnitTestEx.Xunit.Test/PersonFunctionTest.cs +++ b/tests/UnitTestEx.Xunit.Test/PersonFunctionTest.cs @@ -15,6 +15,7 @@ public async Task NoData() { using var test = FunctionTester.Create(); (await test.HttpTrigger() + .WithNoRouteCheck() .RunAsync(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person"), test.Logger))) .AssertOK() .AssertContent("This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."); @@ -25,6 +26,7 @@ public void QueryString() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person?name=Trevor"), test.Logger)) .AssertOK() .AssertContent("Hello, Trevor. This HTTP triggered function executed successfully."); @@ -35,6 +37,7 @@ public void WithBody() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Get, "person", new { name = "Jane" }), test.Logger)) .AssertOK() .AssertContent("Hello, Jane. This HTTP triggered function executed successfully."); @@ -45,6 +48,7 @@ public void BadRequest1() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Post, "person", new { name = "Brian" }), test.Logger)) .AssertBadRequest() .AssertErrors("Name cannot be Brian."); @@ -55,6 +59,7 @@ public void BadRequest2() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Post, "person", new { name = "Brian" }), test.Logger)) .AssertBadRequest() .AssertErrors(new ApiError("name", "Name cannot be Brian.")); @@ -65,6 +70,7 @@ public void BadRequest4() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Post, "person", new { name = "Bruce" }), test.Logger)) .AssertBadRequest() .AssertErrors("Name cannot be Bruce."); @@ -75,6 +81,7 @@ public void ValidJson() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Get, "person", new { name = "Rachel" }), test.Logger)) .AssertOK() .AssertValue(new { FirstName = "Rachel", LastName = "Smith" }); @@ -85,6 +92,7 @@ public void ValidJsonResource() { using var test = FunctionTester.Create(); test.HttpTrigger() + .WithNoRouteCheck() .Run(f => f.Run(test.CreateJsonHttpRequest(HttpMethod.Get, "person", new { name = "Rachel" }), test.Logger)) .AssertOK() .AssertValueFromJsonResource("FunctionTest-ValidJsonResource.json"); diff --git a/tests/UnitTestEx.Xunit.Test/ProductFunctionTest.cs b/tests/UnitTestEx.Xunit.Test/ProductFunctionTest.cs index 78b3723..0cffded 100644 --- a/tests/UnitTestEx.Xunit.Test/ProductFunctionTest.cs +++ b/tests/UnitTestEx.Xunit.Test/ProductFunctionTest.cs @@ -22,7 +22,7 @@ public void Notfound() using var test = FunctionTester.Create(); test.ReplaceHttpClientFactory(mcf) .HttpTrigger() - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/xyz"), "xyz", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/xyz"), "xyz", test.Logger)) .AssertNotFound(); } @@ -36,7 +36,7 @@ public void Success() using var test = FunctionTester.Create(); test.ReplaceHttpClientFactory(mcf) .HttpTrigger() - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/abc"), "abc", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc", test.Logger)) .AssertOK() .AssertValue(new { id = "Abc", description = "A blue carrot" }); } @@ -51,7 +51,7 @@ public void Success2() using var test = FunctionTester.Create(); test.ReplaceHttpClientFactory(mcf) .Type() - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/abc"), "abc", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/abc"), "abc", test.Logger)) .ToActionResultAssertor() .AssertOK() .AssertValue(new { id = "Abc", description = "A blue carrot" }); @@ -65,7 +65,7 @@ public void Exception() using var test = FunctionTester.Create(); test.ReplaceHttpClientFactory(mcf) .HttpTrigger() - .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "person/exception"), "exception", test.Logger)) + .Run(f => f.Run(test.CreateHttpRequest(HttpMethod.Get, "product/exception"), "exception", test.Logger)) .AssertException("An unexpected exception occured."); } }