From fd085ce75f009fc2a90752dea9d31bed7af9d097 Mon Sep 17 00:00:00 2001 From: Federico Colombo Date: Tue, 20 Dec 2022 18:26:14 -0600 Subject: [PATCH] Audit.WebApi / Audit.WebApi.Core: Allow configuring the `AuditDataProvider` as a service in the `IServiceCollection` (#557) --- CHANGELOG.md | 3 + Directory.Build.props | 2 +- .../DbContextHelper.Core.cs | 1 - src/Audit.NET/Configuration.cs | 2 +- .../ConfigurationApi/Configurator.cs | 6 + .../ConfigurationApi/IConfigurator.cs | 4 + src/Audit.WebApi/AuditApiAdapter.Core.cs | 10 +- src/Audit.WebApi/AuditApiAdapter.cs | 5 +- src/Audit.WebApi/AuditMiddleware.cs | 10 +- src/Audit.WebApi/README.md | 23 +++- .../Audit.EntityFramework.UnitTest/EfTests.cs | 6 +- .../Audit.Integration.AspNetCore.csproj | 6 +- .../Controllers/MyController.cs | 10 +- test/Audit.Integration.AspNetCore/Dtos.cs | 8 ++ .../Pages/PageTest/Index.cshtml.cs | 5 - test/Audit.Integration.AspNetCore/Program.cs | 4 + test/Audit.Integration.AspNetCore/Startup.cs | 29 ++++- .../WebApiTests.cs | 27 +++++ .../ActionFilterUnitTest.Core.cs | 35 +++++- .../ActionFilterUnitTest.cs | 35 ++++-- .../Audit.WebApi.UnitTest.csproj | 1 + test/Audit.WebApi.UnitTest/IsolationTests.cs | 105 ++++++++++++++++++ 22 files changed, 302 insertions(+), 35 deletions(-) create mode 100644 test/Audit.Integration.AspNetCore/Dtos.cs create mode 100644 test/Audit.WebApi.UnitTest/IsolationTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e74f27ec..3bd53685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ All notable changes to Audit.NET and its extensions will be documented in this f The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +## [20.1.3] - 2022-12-20 +- Audit.WebApi / Audit.WebApi.Core: Allow configuring the `AuditDataProvider` as a service in the `IServiceCollection` (#557) + ## [20.1.2] - 2022-12-14 - Audit.EntityFramework.Core: Fixing issue with GetColumnName when a property is mapped to JSON using EF Core 7 (#555) diff --git a/Directory.Build.props b/Directory.Build.props index 7e27fce7..75a10794 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 20.1.2 + 20.1.3 diff --git a/src/Audit.EntityFramework/DbContextHelper.Core.cs b/src/Audit.EntityFramework/DbContextHelper.Core.cs index c8876ecb..06fe8dc2 100644 --- a/src/Audit.EntityFramework/DbContextHelper.Core.cs +++ b/src/Audit.EntityFramework/DbContextHelper.Core.cs @@ -140,7 +140,6 @@ private bool HasPropertyValue(IAuditDbContext context, EntityEntry entry, string // property formatted value = settings.FormatProperties[propName].Invoke(currentValue); return true; - } } return false; diff --git a/src/Audit.NET/Configuration.cs b/src/Audit.NET/Configuration.cs index adc5b7e1..09b917ab 100644 --- a/src/Audit.NET/Configuration.cs +++ b/src/Audit.NET/Configuration.cs @@ -102,7 +102,7 @@ static Configuration() #endif }; #endif - SystemClock = new DefaultSystemClock(); + SystemClock = new DefaultSystemClock(); ResetCustomActions(); _auditScopeFactory = new AuditScopeFactory(); } diff --git a/src/Audit.NET/ConfigurationApi/Configurator.cs b/src/Audit.NET/ConfigurationApi/Configurator.cs index cdb62af6..1fb8ca73 100644 --- a/src/Audit.NET/ConfigurationApi/Configurator.cs +++ b/src/Audit.NET/ConfigurationApi/Configurator.cs @@ -28,6 +28,12 @@ public IConfigurator JsonAdapter() where T : IJsonAdapter return this; } + public IConfigurator IncludeStackTrace(bool includeStackTrace = true) + { + Configuration.IncludeStackTrace = includeStackTrace; + return this; + } + public ICreationPolicyConfigurator UseNullProvider() { var dataProvider = new NullDataProvider(); diff --git a/src/Audit.NET/ConfigurationApi/IConfigurator.cs b/src/Audit.NET/ConfigurationApi/IConfigurator.cs index e7f31e3a..d5b116f8 100644 --- a/src/Audit.NET/ConfigurationApi/IConfigurator.cs +++ b/src/Audit.NET/ConfigurationApi/IConfigurator.cs @@ -28,6 +28,10 @@ public interface IConfigurator /// IConfigurator JsonAdapter() where T : IJsonAdapter; /// + /// Globally include the full stack trace in the audit events. + /// + IConfigurator IncludeStackTrace(bool includeStackTrace = true); + /// /// Use a null provider. No audit events will be saved. Useful for testing purposes or to disable the audit logs. /// ICreationPolicyConfigurator UseNullProvider(); diff --git a/src/Audit.WebApi/AuditApiAdapter.Core.cs b/src/Audit.WebApi/AuditApiAdapter.Core.cs index 4b7885ef..c87a0d9a 100644 --- a/src/Audit.WebApi/AuditApiAdapter.Core.cs +++ b/src/Audit.WebApi/AuditApiAdapter.Core.cs @@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Http.Extensions; using System.Runtime.CompilerServices; using Audit.Core.Providers; +using Microsoft.Extensions.DependencyInjection; namespace Audit.WebApi { @@ -74,7 +75,14 @@ internal async Task BeforeExecutingAsync(ActionExecutingContext actionContext, { Action = auditAction }; - var auditScope = await AuditScope.CreateAsync(new AuditScopeOptions() { EventType = eventType, AuditEvent = auditEventAction, CallingMethod = actionDescriptor.MethodInfo }); + var dataProvider = httpContext.RequestServices?.GetService(); + var auditScope = await Core.Configuration.AuditScopeFactory.CreateAsync(new AuditScopeOptions() + { + EventType = eventType, + AuditEvent = auditEventAction, + DataProvider = dataProvider, + CallingMethod = actionDescriptor?.MethodInfo + }); httpContext.Items[AuditApiHelper.AuditApiActionKey] = auditAction; httpContext.Items[AuditApiHelper.AuditApiScopeKey] = auditScope; } diff --git a/src/Audit.WebApi/AuditApiAdapter.cs b/src/Audit.WebApi/AuditApiAdapter.cs index f2b2b13c..746785b6 100644 --- a/src/Audit.WebApi/AuditApiAdapter.cs +++ b/src/Audit.WebApi/AuditApiAdapter.cs @@ -67,14 +67,17 @@ public async Task BeforeExecutingAsync(HttpActionContext actionContext, IContext { Action = auditAction }; + + var dataProvider = contextWrapper.GetHttpContext()?.GetService(typeof(AuditDataProvider)) as AuditDataProvider; var options = new AuditScopeOptions() { EventType = eventType, AuditEvent = auditEventAction, + DataProvider = dataProvider, // the inner ActionDescriptor is of type ReflectedHttpActionDescriptor even when using api versioning: CallingMethod = (actionContext.ActionDescriptor?.ActionBinding?.ActionDescriptor as ReflectedHttpActionDescriptor)?.MethodInfo }; - var auditScope = await AuditScope.CreateAsync(options); + var auditScope = await Configuration.AuditScopeFactory.CreateAsync(options); contextWrapper.Set(AuditApiHelper.AuditApiActionKey, auditAction); contextWrapper.Set(AuditApiHelper.AuditApiScopeKey, auditScope); } diff --git a/src/Audit.WebApi/AuditMiddleware.cs b/src/Audit.WebApi/AuditMiddleware.cs index 68b98ff3..ec25ec77 100644 --- a/src/Audit.WebApi/AuditMiddleware.cs +++ b/src/Audit.WebApi/AuditMiddleware.cs @@ -6,6 +6,7 @@ using System.IO; using Microsoft.AspNetCore.Http.Extensions; using Audit.Core.Extensions; +using Microsoft.Extensions.DependencyInjection; namespace Audit.WebApi { @@ -114,7 +115,13 @@ private async Task BeforeInvoke(HttpContext context, bool includeHeaders, bool i { Action = auditAction }; - var auditScope = await AuditScope.CreateAsync(new AuditScopeOptions() { EventType = eventType, AuditEvent = auditEventAction }); + var dataProvider = context.RequestServices?.GetService(); + var auditScope = await Core.Configuration.AuditScopeFactory.CreateAsync(new AuditScopeOptions() + { + EventType = eventType, + AuditEvent = auditEventAction, + DataProvider = dataProvider + }); context.Items[AuditApiHelper.AuditApiActionKey] = auditAction; context.Items[AuditApiHelper.AuditApiScopeKey] = auditScope; } @@ -158,7 +165,6 @@ private async Task AfterInvoke(HttpContext context, bool includeResponseBody, bo await auditScope.DisposeAsync(); } } - } } #endif diff --git a/src/Audit.WebApi/README.md b/src/Audit.WebApi/README.md index fa77e7ef..ebd42432 100644 --- a/src/Audit.WebApi/README.md +++ b/src/Audit.WebApi/README.md @@ -198,7 +198,28 @@ You can mix the **Audit Middleware** together with the **Global Action Filter** ### Output -The audit events are stored using a _Data Provider_. You can use one of the [available data providers](https://github.com/thepirat000/Audit.NET#data-providers-included) or implement your own. Please refer to the [data providers](https://github.com/thepirat000/Audit.NET#data-providers) section on Audit.NET documentation. +The audit events are stored using a **_Data Provider_**. You can use one of the [available data providers](https://github.com/thepirat000/Audit.NET#data-providers-included) or implement your own. Please refer to the [data providers](https://github.com/thepirat000/Audit.NET#data-providers) section on Audit.NET documentation. + +You can setup the data provider to use by registering an instance of an `AuditDataProdiver` +to the `IServiceCollection` on your start-up code, for example: + +```c# +var dataProvider = new FileDataProvider(cfg => cfg.Directory(@"C:\Logs")); +services.AddSingleton(dataProvider); +``` + +Or, alternatively, you can setup the data provider globally with the static configuration: + +```c# +Audit.Core.Configuration.DataProvider = new FileDataProvider(cfg => cfg.Directory(@"C:\Logs")); +``` + +Or using the fluent API: + +```c# +Audit.Core.Configuration.Setup() + .UseFileLogProvider(cfg => cfg.Directory(@"C:\Logs")); +``` ### Settings (Action Filter) diff --git a/test/Audit.EntityFramework.UnitTest/EfTests.cs b/test/Audit.EntityFramework.UnitTest/EfTests.cs index d56e2179..eb7e00f4 100644 --- a/test/Audit.EntityFramework.UnitTest/EfTests.cs +++ b/test/Audit.EntityFramework.UnitTest/EfTests.cs @@ -238,6 +238,7 @@ public void Test_FunctionMapping() { AuditEventEntityFramework auditEvent = null; Audit.Core.Configuration.Setup() + .IncludeStackTrace(true) .UseDynamicProvider(x => x.OnInsertAndReplace(ev => { auditEvent = ev as AuditEventEntityFramework; @@ -264,6 +265,9 @@ public void Test_FunctionMapping() ctx.SaveChanges(); } + Audit.Core.Configuration.Setup() + .IncludeStackTrace(false); + Assert.AreEqual(1, auditEvent.EntityFrameworkEvent.Entries.Count); // PK is zero because the insertion via SP Assert.IsTrue((int)(auditEvent.EntityFrameworkEvent.Entries[0].PrimaryKey["Id"]) == 0); @@ -271,7 +275,7 @@ public void Test_FunctionMapping() Assert.AreEqual("Insert", auditEvent.EntityFrameworkEvent.Entries[0].Action); Assert.AreEqual("Blogs", auditEvent.EntityFrameworkEvent.Entries[0].Table); Assert.AreEqual(title, auditEvent.EntityFrameworkEvent.Entries[0].ColumnValues["Title"]); - Assert.IsTrue(auditEvent.Environment.CallingMethodName.Contains("Test_FunctionMapping")); + Assert.IsTrue(auditEvent.Environment.StackTrace.Contains("Test_FunctionMapping")); } [Test] diff --git a/test/Audit.Integration.AspNetCore/Audit.Integration.AspNetCore.csproj b/test/Audit.Integration.AspNetCore/Audit.Integration.AspNetCore.csproj index 56df831f..2afcc359 100644 --- a/test/Audit.Integration.AspNetCore/Audit.Integration.AspNetCore.csproj +++ b/test/Audit.Integration.AspNetCore/Audit.Integration.AspNetCore.csproj @@ -1,7 +1,7 @@  - net5.0 + net7.0 ../../src/StrongName/Audit.NET.UnitTests.snk true true @@ -31,7 +31,9 @@ - + + + diff --git a/test/Audit.Integration.AspNetCore/Controllers/MyController.cs b/test/Audit.Integration.AspNetCore/Controllers/MyController.cs index 36b979cb..d504e8ae 100644 --- a/test/Audit.Integration.AspNetCore/Controllers/MyController.cs +++ b/test/Audit.Integration.AspNetCore/Controllers/MyController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Audit.WebApi; +using Microsoft.AspNetCore.JsonPatch; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -24,12 +25,17 @@ public override void OnActionExecuting(ActionExecutingContext context) { scope.Event.CustomFields["ScopeExists"] = true; } + base.OnActionExecuting(context); } - public override void OnActionExecuted(ActionExecutedContext context) + // PATCH api/my/JsonPatch + [AuditApi(IncludeHeaders = true, IncludeResponseBody = true, IncludeRequestBody = true, IncludeModelState = true, IncludeResponseHeaders = true)] + [HttpPatch("JsonPatch")] + public IActionResult JsonPatch([FromBody] JsonPatchDocument patchDoc) { - base.OnActionExecuted(context); + return Ok(); } } + } diff --git a/test/Audit.Integration.AspNetCore/Dtos.cs b/test/Audit.Integration.AspNetCore/Dtos.cs new file mode 100644 index 00000000..0585e597 --- /dev/null +++ b/test/Audit.Integration.AspNetCore/Dtos.cs @@ -0,0 +1,8 @@ +namespace Audit.Integration.AspNetCore +{ + public class Customer + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/test/Audit.Integration.AspNetCore/Pages/PageTest/Index.cshtml.cs b/test/Audit.Integration.AspNetCore/Pages/PageTest/Index.cshtml.cs index 700a51c0..4e276402 100644 --- a/test/Audit.Integration.AspNetCore/Pages/PageTest/Index.cshtml.cs +++ b/test/Audit.Integration.AspNetCore/Pages/PageTest/Index.cshtml.cs @@ -48,9 +48,4 @@ public async Task OnPutAsync([FromBody] Customer customer) } } - public class Customer - { - public int Id { get; set; } - public string Name { get; set; } - } } diff --git a/test/Audit.Integration.AspNetCore/Program.cs b/test/Audit.Integration.AspNetCore/Program.cs index 6a7cc7bc..25dc8798 100644 --- a/test/Audit.Integration.AspNetCore/Program.cs +++ b/test/Audit.Integration.AspNetCore/Program.cs @@ -33,6 +33,10 @@ static async Task MainAsync(string[] args) await webApiTests.TestInitialize(); Console.WriteLine("PASSED - TestInitialize"); + Console.WriteLine("START - Test_WebApi_JsonPatch_Async"); + await webApiTests.Test_WebApi_JsonPatch_Async(); + Console.WriteLine("PASSED - Test_WebApi_JsonPatch_Async"); + Console.WriteLine("START - Test_WebApi_GlobalFilter_SerializeParams_Async"); await webApiTests.Test_WebApi_GlobalFilter_SerializeParams_Async(); Console.WriteLine("PASSED - Test_WebApi_GlobalFilter_SerializeParams_Async"); diff --git a/test/Audit.Integration.AspNetCore/Startup.cs b/test/Audit.Integration.AspNetCore/Startup.cs index cf86ebb8..85a2963d 100644 --- a/test/Audit.Integration.AspNetCore/Startup.cs +++ b/test/Audit.Integration.AspNetCore/Startup.cs @@ -1,4 +1,6 @@ -using Audit.WebApi; +using System.Linq; +using Audit.Core; +using Audit.WebApi; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.Features; @@ -7,6 +9,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; using Audit.Mvc; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.Options; +using Microsoft.CodeAnalysis; namespace Audit.Integration.AspNetCore { @@ -22,7 +28,6 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.Configure(options => { options.ValueCountLimit = 2; @@ -30,6 +35,8 @@ public void ConfigureServices(IServiceCollection services) services.AddMvc(mvc => { + mvc.InputFormatters.Insert(0, GetJsonPatchInputFormatter()); + mvc.EnableEndpointRouting = false; mvc.Filters.Add(new AuditIgnoreActionFilter_ForTest()); mvc.Filters.Add(new AuditApiGlobalFilter(config => config @@ -43,7 +50,7 @@ public void ConfigureServices(IServiceCollection services) .IncludeResponseHeaders() .IncludeResponseBody(ctx => ctx.HttpContext.Response.StatusCode == 200) .IncludeRequestBody())); - + mvc.Filters.Add(new AuditApiGlobalFilter(config => config .LogActionIf(d => d.ControllerName == "Values" && d.ActionName == "TestSerializeParams") .SerializeActionParameters(true) @@ -101,5 +108,21 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseMvc(); } + + private static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter() + { + var builder = new ServiceCollection() + .AddLogging() + .AddMvc() + .AddNewtonsoftJson() + .Services.BuildServiceProvider(); + + return builder + .GetRequiredService>() + .Value + .InputFormatters + .OfType() + .First(); + } } } diff --git a/test/Audit.Integration.AspNetCore/WebApiTests.cs b/test/Audit.Integration.AspNetCore/WebApiTests.cs index b9220c7a..1a11f951 100644 --- a/test/Audit.Integration.AspNetCore/WebApiTests.cs +++ b/test/Audit.Integration.AspNetCore/WebApiTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Text; @@ -8,6 +9,7 @@ using Audit.Core.Providers; using Audit.Integration.AspNetCore.Pages.Test; using Audit.WebApi; +using Microsoft.AspNetCore.JsonPatch; using Microsoft.AspNetCore.Mvc.Controllers; using Newtonsoft.Json; using NUnit.Framework; @@ -900,5 +902,30 @@ public async Task Test_WebApi_GlobalFilter_DoNotSerializeParams_Async() Assert.AreEqual(1, events[0].Action.ActionParameters.Count); Assert.AreEqual(-1, (events[0].Action.ActionParameters["customer"] as Customer)?.Id); } + + public async Task Test_WebApi_JsonPatch_Async() + { + Audit.Core.Configuration.Setup() + .UseInMemoryProvider() + .WithCreationPolicy(EventCreationPolicy.InsertOnEnd); + + var client = new HttpClient(); + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(e => e.Name, "NewValue"); + var bodyJson = JsonConvert.SerializeObject(patchDoc); + + var result = await client.PatchAsync($"http://localhost:{_port}/api/My/JsonPatch", new StringContent(bodyJson, Encoding.UTF8, "application/json-patch+json")); + + var events = (Configuration.DataProvider as InMemoryDataProvider)?.GetAllEventsOfType(); + var eventJson = events?.FirstOrDefault()?.ToJson(); + var op = (events?[0].Action.ActionParameters.First().Value as JsonPatchDocument)?.Operations[0]; + + Assert.IsNotNull(events); + Assert.IsNotNull(eventJson); + Assert.IsNotNull(op); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); + Assert.AreEqual(1, events.Count); + Assert.AreEqual("NewValue", op.value); + } } } \ No newline at end of file diff --git a/test/Audit.WebApi.UnitTest/ActionFilterUnitTest.Core.cs b/test/Audit.WebApi.UnitTest/ActionFilterUnitTest.Core.cs index b7d83e9e..e9a548b1 100644 --- a/test/Audit.WebApi.UnitTest/ActionFilterUnitTest.Core.cs +++ b/test/Audit.WebApi.UnitTest/ActionFilterUnitTest.Core.cs @@ -72,8 +72,9 @@ public void Test_AuditApiActionFilter_ShouldIncludeResponseBody() } } - [Test] - public async Task Test_AuditApiActionFilter_InsertOnEnd() + [TestCase(true)] + [TestCase(false)] + public async Task Test_AuditApiActionFilter_InsertOnEnd(bool injectDataProvider) { // Mock out the context to run the action filter. var request = new Mock(); @@ -95,6 +96,25 @@ public async Task Test_AuditApiActionFilter_InsertOnEnd() httpContext.SetupGet(c => c.Request).Returns(request.Object); httpContext.SetupGet(c => c.Items).Returns(() => itemsDict); httpContext.SetupGet(c => c.Response).Returns(() => httpResponse.Object); + + var dataProvider = new Mock(); + dataProvider.Setup(x => x.InsertEventAsync(It.IsAny())).ReturnsAsync(() => Task.FromResult(Guid.NewGuid())); + + Mock svcProvider = null; + if (injectDataProvider) + { + Audit.Core.Configuration.DataProvider = null; + svcProvider = new Mock(); + svcProvider.Setup(s => s.GetService(It.IsAny())) + .Returns((Type t) => t == typeof(AuditDataProvider) ? dataProvider.Object : null); + + httpContext.SetupGet(c => c.RequestServices).Returns(() => svcProvider.Object); + } + else + { + Audit.Core.Configuration.DataProvider = dataProvider.Object; + } + var ci = new Mock(); ci.SetupGet(_ => _.RemoteIpAddress).Returns(() => null); httpContext.SetupGet(c => c.Connection).Returns(() => ci.Object); @@ -117,9 +137,7 @@ public async Task Test_AuditApiActionFilter_InsertOnEnd() }; var filters = new List(); var controller = new Mock(); - var dataProvider = new Mock(); - dataProvider.Setup(x => x.InsertEventAsync(It.IsAny())).ReturnsAsync(() => Task.FromResult(Guid.NewGuid())); - Audit.Core.Configuration.DataProvider = dataProvider.Object; + Audit.Core.Configuration.CreationPolicy = EventCreationPolicy.InsertOnEnd; var filter = new AuditApiAttribute() @@ -144,6 +162,10 @@ public async Task Test_AuditApiActionFilter_InsertOnEnd() dataProvider.Verify(p => p.InsertEventAsync(It.IsAny()), Times.Once); dataProvider.Verify(p => p.ReplaceEvent(It.IsAny(), It.IsAny()), Times.Never); dataProvider.Verify(p => p.ReplaceEventAsync(It.IsAny(), It.IsAny()), Times.Never); + if (injectDataProvider) + { + svcProvider.Verify(p => p.GetService(It.IsAny()), Times.AtLeastOnce); + } Assert.IsNotNull(action.ActionExecutingContext); Assert.AreEqual((actionContext.ActionDescriptor as ControllerActionDescriptor).ActionName, (action.ActionExecutingContext.ActionDescriptor as ControllerActionDescriptor).ActionName); Assert.AreEqual((actionContext.ActionDescriptor as ControllerActionDescriptor).ControllerName, (action.ActionExecutingContext.ActionDescriptor as ControllerActionDescriptor).ControllerName); @@ -218,6 +240,7 @@ public async Task Test_AuditApiActionFilter_InheritedResultType() } [Test] + [NonParallelizable] public async Task Test_AuditApiActionFilter_Manual() { // Mock out the context to run the action filter. @@ -273,6 +296,8 @@ public async Task Test_AuditApiActionFilter_Manual() await filter.OnActionExecutionAsync(actionExecutingContext, async () => await Task.FromResult(actionExecutedContext)); + Audit.Core.Configuration.CreationPolicy = EventCreationPolicy.InsertOnEnd; + var action = itemsDict["__private_AuditApiAction__"] as AuditApiAction; var scope = itemsDict["__private_AuditApiScope__"] as AuditScope; diff --git a/test/Audit.WebApi.UnitTest/ActionFilterUnitTest.cs b/test/Audit.WebApi.UnitTest/ActionFilterUnitTest.cs index 4dbfb65d..08afc08c 100644 --- a/test/Audit.WebApi.UnitTest/ActionFilterUnitTest.cs +++ b/test/Audit.WebApi.UnitTest/ActionFilterUnitTest.cs @@ -14,6 +14,8 @@ using System.Threading.Tasks; using System.Linq; using System.Collections.Specialized; +using System.Web.Http; +using System.Web.Http.Dependencies; namespace Audit.WebApi.UnitTest { @@ -70,13 +72,13 @@ public void Test_AuditApiActionFilter_ShouldIncludeResponseBody() } } - [Test] - public async Task Test_AuditApiActionFilter_InsertOnEnd() + [TestCase(true)] + [TestCase(false)] + public async Task Test_AuditApiActionFilter_InsertOnEnd(bool injectDataProvider) { // Mock out the context to run the action filter. var request = new Mock(); - //var request = new HttpRequest(null, "http://200.10.10.20:1010/api/values", null); request.Setup(c => c.ContentType).Returns("application/json"); var stream = new MemoryStream(); var writer = new StreamWriter(stream); @@ -89,12 +91,14 @@ public async Task Test_AuditApiActionFilter_InsertOnEnd() var httpResponse = new Mock(); httpResponse.Setup(c => c.StatusCode).Returns(200); - httpResponse.Setup(c => c.Headers).Returns(new NameValueCollection() { {"header-one", "1" }, { "header-two", "2" } }); + httpResponse.Setup(c => c.Headers).Returns(new NameValueCollection() + { { "header-one", "1" }, { "header-two", "2" } }); var itemsDict = new Dictionary(); var httpContext = new Mock(); httpContext.SetupGet(c => c.Request).Returns(request.Object); httpContext.SetupGet(c => c.Items).Returns(() => itemsDict); httpContext.SetupGet(c => c.Response).Returns(() => httpResponse.Object); + var controllerContext = new HttpControllerContext() { ControllerDescriptor = new HttpControllerDescriptor() @@ -112,12 +116,13 @@ public async Task Test_AuditApiActionFilter_InsertOnEnd() var args = new Dictionary() { - {"test1", "value1" } + { "test1", "value1" } }; - + var dataProvider = new Mock(); - dataProvider.Setup(x => x.InsertEventAsync(It.IsAny())).ReturnsAsync(() => Task.FromResult(Guid.NewGuid())); - Audit.Core.Configuration.DataProvider = dataProvider.Object; + dataProvider.Setup(x => x.InsertEventAsync(It.IsAny())) + .ReturnsAsync(() => Task.FromResult(Guid.NewGuid())); + Audit.Core.Configuration.CreationPolicy = EventCreationPolicy.InsertOnEnd; var filter = new AuditApiAttribute() { @@ -133,11 +138,23 @@ public async Task Test_AuditApiActionFilter_InsertOnEnd() { ActionDescriptor = actionDescriptor, ControllerContext = controllerContext, - + }; var actionExecutingContext = new HttpActionContext(controllerContext, actionDescriptor); actionExecutingContext.ActionArguments.Add("test1", "value1"); actionExecutingContext.Request.Properties.Add("MS_HttpContext", httpContext.Object); + + if (injectDataProvider) + { + Audit.Core.Configuration.DataProvider = null; + httpContext.Setup(h => h.GetService(typeof(AuditDataProvider))) + .Returns(dataProvider.Object); + } + else + { + Audit.Core.Configuration.DataProvider = dataProvider.Object; + } + var response = new HttpResponseMessage(HttpStatusCode.OK); response.Headers.Add("header-one", "1"); response.Headers.Add("header-two", "2"); diff --git a/test/Audit.WebApi.UnitTest/Audit.WebApi.UnitTest.csproj b/test/Audit.WebApi.UnitTest/Audit.WebApi.UnitTest.csproj index 7acf2f1d..87b963e0 100644 --- a/test/Audit.WebApi.UnitTest/Audit.WebApi.UnitTest.csproj +++ b/test/Audit.WebApi.UnitTest/Audit.WebApi.UnitTest.csproj @@ -58,6 +58,7 @@ + diff --git a/test/Audit.WebApi.UnitTest/IsolationTests.cs b/test/Audit.WebApi.UnitTest/IsolationTests.cs new file mode 100644 index 00000000..2fc03887 --- /dev/null +++ b/test/Audit.WebApi.UnitTest/IsolationTests.cs @@ -0,0 +1,105 @@ +#if NET5_0_OR_GREATER +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Audit.Core; +using Audit.Core.Providers; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using NUnit.Framework; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; + +namespace Audit.WebApi.UnitTest +{ + [Route("[controller]/[action]")] + [ApiController] + public class TestIsolationController : ControllerBase + { + [AuditApi] + [HttpGet] + public IActionResult Action_AuditApiAttribute([FromQuery] string q) + { + return Ok(q); + } + + public IActionResult Action_Middleware([FromQuery] string q) + { + return Ok(q); + } + } + + public class TestHelper + { + public static TestServer GetTestServer(AuditDataProvider dataProvider) + { + return new TestServer(WebHost.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddSingleton(dataProvider); + services.AddControllers(); + }) + .Configure((ctx, app) => + { + app.UseAuditMiddleware(cfg => cfg + .FilterByRequest(r => r.Path.Value?.Contains("Action_Middleware") == true)); + app.UseRouting(); + app.UseEndpoints(e => + { + e.MapControllers(); + }); + }) + ); + } + } + + [Parallelizable] + public class IsolationTests + { + [TestCase(10)] + public async Task Test_Isolation_InjectDataProvider_AuditApiAttribute_Parallel(int count) + { + var tasks = Enumerable.Range(1, count).Select(_ => Test_Isolation_InjectDataProvider_AuditApiAttribute_TestOne()).ToArray(); + await Task.WhenAll(tasks); + } + + private async Task Test_Isolation_InjectDataProvider_AuditApiAttribute_TestOne() + { + var guid = Guid.NewGuid(); + var dataProvider = new InMemoryDataProvider(); + using var app = TestHelper.GetTestServer(dataProvider); + using var client = app.CreateClient(); + + var response = await client.GetAsync($"/TestIsolation/Action_AuditApiAttribute?q={guid}"); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual(1, dataProvider.GetAllEvents().Count); + Assert.AreEqual(guid.ToString(), dataProvider.GetAllEventsOfType().First().Action.ActionParameters["q"].ToString()); + } + + [TestCase(10)] + public async Task Test_Isolation_InjectDataProvider_Middleware_Parallel(int count) + { + var tasks = Enumerable.Range(1, count).Select(_ => Test_Isolation_InjectDataProvider_Middleware_TestOne()).ToArray(); + await Task.WhenAll(tasks); + } + + private async Task Test_Isolation_InjectDataProvider_Middleware_TestOne() + { + var guid = Guid.NewGuid(); + var dataProvider = new InMemoryDataProvider(); + using var app = TestHelper.GetTestServer(dataProvider); + using var client = app.CreateClient(); + + var response = await client.GetAsync($"/TestIsolation/Action_Middleware?q={guid}"); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual(1, dataProvider.GetAllEvents().Count); + Assert.IsTrue(dataProvider.GetAllEventsOfType().First().Action.RequestUrl.Contains(guid.ToString())); + } + } +} +#endif \ No newline at end of file