From d33edbdd85def7ec5d80595c7ee85831e446be14 Mon Sep 17 00:00:00 2001 From: Brant DeBow Date: Thu, 6 Mar 2025 20:29:18 -0500 Subject: [PATCH] Added new TemplateProcessor and added/updated unit tests --- ...ationIntegrationConfigurationRepository.cs | 2 +- .../Implementations/WebhookEventHandler.cs | 8 +- .../Utilities/TemplateProcessor.cs | 20 ++++ src/Core/Core.csproj | 1 - .../Services/WebhookEventHandlerTests.cs | 55 ++++++++--- .../Utilities/TemplateProcessorTests.cs | 91 +++++++++++++++++++ 6 files changed, 160 insertions(+), 17 deletions(-) create mode 100644 src/Core/AdminConsole/Utilities/TemplateProcessor.cs create mode 100644 test/Core.Test/AdminConsole/Utilities/TemplateProcessorTests.cs diff --git a/src/Core/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs b/src/Core/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs index af33b123c7..03e3ae2bf4 100644 --- a/src/Core/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs +++ b/src/Core/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs @@ -46,7 +46,7 @@ public class OrganizationIntegrationConfigurationRepository(GlobalSettings globa { Url = _webhookUrl, }, - Template = "{ \"newObject\": true }" + Template = "{ \"Date\": \"#Date#\", \"Type\": \"#Type#\", \"UserId\": \"#UserId#\" }" } as IntegrationConfiguration; default: return null; diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs index 4ad3713107..95e668e5be 100644 --- a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs @@ -1,4 +1,4 @@ -using System.Net.Http.Json; +using System.Text; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Integrations; @@ -27,7 +27,11 @@ public class WebhookEventHandler( eventMessage.Type); if (configuration is not null) { - var content = JsonContent.Create(configuration.Template); + var content = new StringContent( + TemplateProcessor.ReplaceTokens(configuration.Template, eventMessage), + Encoding.UTF8, + "application/json" + ); var response = await _httpClient.PostAsync( configuration.Configuration.Url, content); diff --git a/src/Core/AdminConsole/Utilities/TemplateProcessor.cs b/src/Core/AdminConsole/Utilities/TemplateProcessor.cs new file mode 100644 index 0000000000..e7613a3f20 --- /dev/null +++ b/src/Core/AdminConsole/Utilities/TemplateProcessor.cs @@ -0,0 +1,20 @@ +using System.Text.RegularExpressions; + +public static class TemplateProcessor +{ + private static readonly Regex TokenRegex = new(@"#(\w+)#", RegexOptions.Compiled); + + public static string ReplaceTokens(string template, object values) + { + if (string.IsNullOrEmpty(template) || values == null) + return template; + + var type = values.GetType(); + return TokenRegex.Replace(template, match => + { + var propertyName = match.Groups[1].Value; + var property = type.GetProperty(propertyName); + return property?.GetValue(values)?.ToString() ?? match.Value; + }); + } +} diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index a271b41784..8a8de3d77d 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -74,7 +74,6 @@ - diff --git a/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs index 6c7d7178c1..0dc4a9495a 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs @@ -1,6 +1,9 @@ using System.Net; using System.Net.Http.Json; +using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Integrations; +using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -8,7 +11,6 @@ using Bit.Test.Common.Helpers; using Bit.Test.Common.MockedHttpClient; using NSubstitute; using Xunit; -using GlobalSettings = Bit.Core.Settings.GlobalSettings; namespace Bit.Core.Test.Services; @@ -16,7 +18,7 @@ namespace Bit.Core.Test.Services; public class WebhookEventHandlerTests { private readonly MockedHttpMessageHandler _handler; - private HttpClient _httpClient; + private readonly HttpClient _httpClient; private const string _webhookUrl = "http://localhost/test/event"; @@ -29,16 +31,26 @@ public class WebhookEventHandlerTests _httpClient = _handler.ToHttpClient(); } - public SutProvider GetSutProvider() + private SutProvider GetSutProvider() { var clientFactory = Substitute.For(); clientFactory.CreateClient(WebhookEventHandler.HttpClientName).Returns(_httpClient); - var globalSettings = new GlobalSettings(); - globalSettings.EventLogging.WebhookUrl = _webhookUrl; + var repository = Substitute.For(); + repository.GetConfigurationAsync( + Arg.Any(), + IntegrationType.Webhook, + Arg.Any() + ).Returns( + new IntegrationConfiguration + { + Configuration = new WebhookConfiguration { ApiKey = "", Url = _webhookUrl }, + Template = "{ \"Date\": \"#Date#\", \"Type\": \"#Type#\", \"UserId\": \"#UserId#\" }" + } + ); return new SutProvider() - .SetDependency(globalSettings) + .SetDependency(repository) .SetDependency(clientFactory) .Create(); } @@ -50,21 +62,22 @@ public class WebhookEventHandlerTests await sutProvider.Sut.HandleEventAsync(eventMessage); sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) + Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) ); Assert.Single(_handler.CapturedRequests); var request = _handler.CapturedRequests[0]; Assert.NotNull(request); - var returned = await request.Content.ReadFromJsonAsync(); + var returned = await request.Content.ReadFromJsonAsync(); + var expected = MockEvent.From(eventMessage); Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - AssertHelper.AssertPropertyEqual(eventMessage, returned, new[] { "IdempotencyId" }); + AssertHelper.AssertPropertyEqual(expected, returned); } [Theory, BitAutoData] - public async Task HandleEventManyAsync_PostsEventsToUrl(IEnumerable eventMessages) + public async Task HandleEventManyAsync_PostsEventsToUrl(List eventMessages) { var sutProvider = GetSutProvider(); @@ -73,13 +86,29 @@ public class WebhookEventHandlerTests Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) ); - Assert.Single(_handler.CapturedRequests); var request = _handler.CapturedRequests[0]; Assert.NotNull(request); - var returned = request.Content.ReadFromJsonAsAsyncEnumerable(); + var returned = await request.Content.ReadFromJsonAsync(); + var expected = MockEvent.From(eventMessages.First()); Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - AssertHelper.AssertPropertyEqual(eventMessages, returned, new[] { "IdempotencyId" }); + AssertHelper.AssertPropertyEqual(expected, returned); + } +} + +public class MockEvent(string date, string type, string userId) +{ + public string Date { get; set; } = date; + public string Type { get; set; } = type; + public string UserId { get; set; } = userId; + + public static MockEvent From(EventMessage eventMessage) + { + return new MockEvent( + eventMessage.Date.ToString(), + eventMessage.Type.ToString(), + eventMessage.UserId.ToString() + ); } } diff --git a/test/Core.Test/AdminConsole/Utilities/TemplateProcessorTests.cs b/test/Core.Test/AdminConsole/Utilities/TemplateProcessorTests.cs new file mode 100644 index 0000000000..46b2658cdb --- /dev/null +++ b/test/Core.Test/AdminConsole/Utilities/TemplateProcessorTests.cs @@ -0,0 +1,91 @@ +using Bit.Core.Models.Data; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Utilities; + +public class TemplateProcessorTests +{ + [Theory, BitAutoData] + public void ReplaceTokens_ReplacesSingleToken(EventMessage eventMessage) + { + var template = "Event #Type# occurred."; + var expected = $"Event {eventMessage.Type} occurred."; + var result = TemplateProcessor.ReplaceTokens(template, eventMessage); + + Assert.Equal(expected, result); + } + + [Theory, BitAutoData] + public void ReplaceTokens_ReplacesMultipleTokens(EventMessage eventMessage) + { + var template = "Event #Type#, User (id: #UserId#)."; + var expected = $"Event {eventMessage.Type}, User (id: {eventMessage.UserId})."; + var result = TemplateProcessor.ReplaceTokens(template, eventMessage); + + Assert.Equal(expected, result); + } + + [Theory, BitAutoData] + public void ReplaceTokens_LeavesUnknownTokensUnchanged(EventMessage eventMessage) + { + var template = "Event #Type#, User (id: #UserId#), Details: #UnknownKey#."; + var expected = $"Event {eventMessage.Type}, User (id: {eventMessage.UserId}), Details: #UnknownKey#."; + var result = TemplateProcessor.ReplaceTokens(template, eventMessage); + + Assert.Equal(expected, result); + } + + [Theory, BitAutoData] + public void ReplaceTokens_WithNullProperty_LeavesTokenUnchanged(EventMessage eventMessage) + { + eventMessage.UserId = null; // Ensure UserId is null for this test + + var template = "Event #Type#, User (id: #UserId#)."; + var expected = $"Event {eventMessage.Type}, User (id: #UserId#)."; + var result = TemplateProcessor.ReplaceTokens(template, eventMessage); + + Assert.Equal(expected, result); + } + + [Theory, BitAutoData] + public void ReplaceTokens_IgnoresCaseSensitiveTokens(EventMessage eventMessage) + { + var template = "Event #type#, User (id: #UserId#)."; // Lowercase "type" + var expected = $"Event #type#, User (id: {eventMessage.UserId})."; // Token remains unchanged + var result = TemplateProcessor.ReplaceTokens(template, eventMessage); + + Assert.Equal(expected, result); + } + + [Theory, BitAutoData] + public void ReplaceTokens_ReturnsOriginalString_IfNoTokensPresent(EventMessage eventMessage) + { + var template = "System is running normally."; + var expected = "System is running normally."; + var result = TemplateProcessor.ReplaceTokens(template, eventMessage); + + Assert.Equal(expected, result); + } + + [Theory, BitAutoData] + public void ReplaceTokens_ReturnsOriginalString_IfTemplateIsNullOrEmpty(EventMessage eventMessage) + { + var emptyTemplate = ""; + var expectedEmpty = ""; + + Assert.Equal(expectedEmpty, TemplateProcessor.ReplaceTokens(emptyTemplate, eventMessage)); + Assert.Null(TemplateProcessor.ReplaceTokens(null, eventMessage)); + } + + [Fact] + public void ReplaceTokens_ReturnsOriginalString_IfDataObjectIsNull() + { + var template = "Event #Type#, User (id: #UserId#)."; + var expected = "Event #Type#, User (id: #UserId#)."; + + var result = TemplateProcessor.ReplaceTokens(template, null); + + Assert.Equal(expected, result); + } +}