diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs index fe33c45156d1..c44e550d15b7 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs @@ -1,8 +1,8 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; @@ -36,13 +36,18 @@ public class IntegrationTemplateContext(EventMessage eventMessage) public string DateIso8601 => Date.ToString("o"); public string EventMessage => JsonSerializer.Serialize(Event); - public User? User { get; set; } + public OrganizationUserUserDetails? User { get; set; } public string? UserName => User?.Name; public string? UserEmail => User?.Email; + public OrganizationUserType? UserType => User?.Type; - public User? ActingUser { get; set; } + public OrganizationUserUserDetails? ActingUser { get; set; } public string? ActingUserName => ActingUser?.Name; public string? ActingUserEmail => ActingUser?.Email; + public OrganizationUserType? ActingUserType => ActingUser?.Type; + + public Group? Group { get; set; } + public string? GroupName => Group?.Name; public Organization? Organization { get; set; } public string? OrganizationName => Organization?.DisplayName(); diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index b17de3c51d3b..142c6855a394 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -96,4 +96,15 @@ UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, /// Accepted OrganizationUser to confirm /// True, if the user was updated. False, if not performed. Task ConfirmOrganizationUserAsync(OrganizationUser organizationUser); + + /// + /// Returns the OrganizationUserUserDetails if found. + /// + /// The id of the organization + /// The id of the User to fetch + /// OrganizationUserUserDetails of the specified user or null if not found + /// + /// Similar to GetByOrganizationAsync, but returns the user details. + /// + Task GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId); } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs index 8423652eb81d..e29d0eaaadf0 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Enums; using Bit.Core.Models.Data; @@ -13,8 +14,9 @@ public class EventIntegrationHandler( IEventIntegrationPublisher eventIntegrationPublisher, IIntegrationFilterService integrationFilterService, IIntegrationConfigurationDetailsCache configurationCache, - IUserRepository userRepository, + IGroupRepository groupRepository, IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, ILogger> logger) : IEventMessageHandler { @@ -89,19 +91,35 @@ private async Task BuildContextAsync(EventMessage ev { var context = new IntegrationTemplateContext(eventMessage); + if (IntegrationTemplateProcessor.TemplateRequiresGroup(template) && eventMessage.GroupId.HasValue) + { + context.Group = await groupRepository.GetByIdAsync(eventMessage.GroupId.Value); + } + + if (eventMessage.OrganizationId is not Guid organizationId) + { + return context; + } + if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue) { - context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value); + context.User = await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( + organizationId: organizationId, + userId: eventMessage.UserId.Value + ); } if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) { - context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value); + context.ActingUser = await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( + organizationId: organizationId, + userId: eventMessage.ActingUserId.Value + ); } - if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue) + if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template)) { - context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value); + context.Organization = await organizationRepository.GetByIdAsync(organizationId); } return context; diff --git a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs index b561e58a869c..62df3b2bc9f0 100644 --- a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs +++ b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs @@ -26,7 +26,7 @@ public static string ReplaceTokens(string template, object values) return match.Value; // Return unknown keys as keys - i.e. #Key# } - return property?.GetValue(values)?.ToString() ?? ""; + return property.GetValue(values)?.ToString() ?? string.Empty; }); } @@ -38,7 +38,8 @@ public static bool TemplateRequiresUser(string template) } return template.Contains("#UserName#", StringComparison.Ordinal) - || template.Contains("#UserEmail#", StringComparison.Ordinal); + || template.Contains("#UserEmail#", StringComparison.Ordinal) + || template.Contains("#UserType#", StringComparison.Ordinal); } public static bool TemplateRequiresActingUser(string template) @@ -49,7 +50,18 @@ public static bool TemplateRequiresActingUser(string template) } return template.Contains("#ActingUserName#", StringComparison.Ordinal) - || template.Contains("#ActingUserEmail#", StringComparison.Ordinal); + || template.Contains("#ActingUserEmail#", StringComparison.Ordinal) + || template.Contains("#ActingUserType#", StringComparison.Ordinal); + } + + public static bool TemplateRequiresGroup(string template) + { + if (string.IsNullOrEmpty(template)) + { + return false; + } + + return template.Contains("#GroupName#", StringComparison.Ordinal); } public static bool TemplateRequiresOrganization(string template) diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index dc4fc74ff8a9..2f2af1322721 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -686,4 +686,21 @@ public async Task ConfirmOrganizationUserAsync(OrganizationUser organizati return rowCount > 0; } + + public async Task GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var result = await connection.QuerySingleOrDefaultAsync( + "[dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId]", + new + { + OrganizationId = organizationId, + UserId = userId + }, + commandType: CommandType.StoredProcedure); + + return result; + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index b871ec44bf9d..e034648cd21f 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -962,4 +962,20 @@ public async Task ConfirmOrganizationUserAsync(Core.Entities.OrganizationU return true; } + +#nullable enable + + public async Task GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var view = new OrganizationUserUserDetailsViewQuery(); + var entity = await view.Run(dbContext).FirstOrDefaultAsync(ou => ou.OrganizationId == organizationId && ou.UserId == userId); + return entity; + } + } +#nullable disable + + } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index ef143b042cb2..c9d4e613b72f 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Models.Teams; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.NoopImplementations; @@ -898,8 +899,9 @@ private static IServiceCollection AddAzureServiceBusIntegration(), integrationFilterService: provider.GetRequiredService(), configurationCache: provider.GetRequiredService(), - userRepository: provider.GetRequiredService(), + groupRepository: provider.GetRequiredService(), organizationRepository: provider.GetRequiredService(), + organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>() ) ); @@ -1025,8 +1027,9 @@ private static IServiceCollection AddRabbitMqIntegration(), integrationFilterService: provider.GetRequiredService(), configurationCache: provider.GetRequiredService(), - userRepository: provider.GetRequiredService(), + groupRepository: provider.GetRequiredService(), organizationRepository: provider.GetRequiredService(), + organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>() ) ); diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql new file mode 100644 index 000000000000..0026d73ed007 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql @@ -0,0 +1,18 @@ +CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + TOP 1 * +FROM + [dbo].[OrganizationUserUserDetailsView] +WHERE + [OrganizationId] = @OrganizationId +AND + [UserId] = @UserId +END +go + diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs index cdb109e28597..d9a3cd6e8a67 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs @@ -2,8 +2,8 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Entities; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; @@ -35,7 +35,7 @@ public void DateIso8601_ReturnsIso8601FormattedDate(EventMessage eventMessage) } [Theory, BitAutoData] - public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, User user) + public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, OrganizationUserUserDetails user) { var sut = new IntegrationTemplateContext(eventMessage) { User = user }; @@ -51,7 +51,7 @@ public void UserName_WhenUserIsNull_ReturnsNull(EventMessage eventMessage) } [Theory, BitAutoData] - public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, User user) + public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, OrganizationUserUserDetails user) { var sut = new IntegrationTemplateContext(eventMessage) { User = user }; @@ -67,7 +67,23 @@ public void UserEmail_WhenUserIsNull_ReturnsNull(EventMessage eventMessage) } [Theory, BitAutoData] - public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, User actingUser) + public void UserType_WhenUserIsSet_ReturnsType(EventMessage eventMessage, OrganizationUserUserDetails user) + { + var sut = new IntegrationTemplateContext(eventMessage) { User = user }; + + Assert.Equal(user.Type, sut.UserType); + } + + [Theory, BitAutoData] + public void UserType_WhenUserIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { User = null }; + + Assert.Null(sut.UserType); + } + + [Theory, BitAutoData] + public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, OrganizationUserUserDetails actingUser) { var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser }; @@ -83,7 +99,7 @@ public void ActingUserName_WhenActingUserIsNull_ReturnsNull(EventMessage eventMe } [Theory, BitAutoData] - public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, User actingUser) + public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, OrganizationUserUserDetails actingUser) { var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser }; @@ -98,6 +114,22 @@ public void ActingUserEmail_WhenActingUserIsNull_ReturnsNull(EventMessage eventM Assert.Null(sut.ActingUserEmail); } + [Theory, BitAutoData] + public void ActingUserType_WhenActingUserIsSet_ReturnsType(EventMessage eventMessage, OrganizationUserUserDetails actingUser) + { + var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser }; + + Assert.Equal(actingUser.Type, sut.ActingUserType); + } + + [Theory, BitAutoData] + public void ActingUserType_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null }; + + Assert.Null(sut.ActingUserType); + } + [Theory, BitAutoData] public void OrganizationName_WhenOrganizationIsSet_ReturnsDisplayName(EventMessage eventMessage, Organization organization) { @@ -113,4 +145,20 @@ public void OrganizationName_WhenOrganizationIsNull_ReturnsNull(EventMessage eve Assert.Null(sut.OrganizationName); } + + [Theory, BitAutoData] + public void GroupName_WhenGroupIsSet_ReturnsName(EventMessage eventMessage, Group group) + { + var sut = new IntegrationTemplateContext(eventMessage) { Group = group }; + + Assert.Equal(group.Name, sut.GroupName); + } + + [Theory, BitAutoData] + public void GroupName_WhenGroupIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { Group = null }; + + Assert.Null(sut.GroupName); + } } diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index 1d94d58aa5f9..c556c1fae0b8 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -1,10 +1,11 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Entities; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -20,9 +21,11 @@ namespace Bit.Core.Test.Services; public class EventIntegrationHandlerTests { private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#"; + private const string _templateWithGroup = "Group: #GroupName#"; private const string _templateWithOrganization = "Org: #OrganizationName#"; - private const string _templateWithUser = "#UserName#, #UserEmail#"; - private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; + private const string _templateWithUser = "#UserName#, #UserEmail#, #UserType#"; + private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#, #ActingUserType#"; + private static readonly Guid _groupId = Guid.NewGuid(); private static readonly Guid _organizationId = Guid.NewGuid(); private static readonly Uri _uri = new Uri("https://localhost"); private static readonly Uri _uri2 = new Uri("https://example.com"); @@ -45,7 +48,7 @@ private SutProvider expectedMessage(string template) + private static IntegrationMessage ExpectedMessage(string template) { return new IntegrationMessage() { @@ -105,7 +108,7 @@ private static List ValidFilterConf config.Configuration = null; config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.Template = _templateBase; - config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup() { }); + config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()); return [config]; } @@ -138,15 +141,16 @@ public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegra await sutProvider.Sut.HandleEventAsync(eventMessage); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -157,7 +161,7 @@ public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegr await sutProvider.Sut.HandleEventAsync(eventMessage); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( @@ -167,29 +171,56 @@ await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage) { var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); - var user = Substitute.For(); + var user = Substitute.For(); user.Email = "test@example.com"; user.Name = "Test"; eventMessage.OrganizationId = _organizationId; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); + sutProvider.GetDependency() + .GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()).Returns(user); await sutProvider.Sut.HandleEventAsync(eventMessage); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}"); Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty); + await sutProvider.GetDependency().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), eventMessage.ActingUserId ?? Guid.Empty); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_GroupTemplate_LoadsGroupFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup)); + var group = Substitute.For(); + group.Name = "Test"; + eventMessage.GroupId = _groupId; + eventMessage.OrganizationId = _organizationId; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(group); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Group: {group.Name}"); + + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.GroupId ?? Guid.Empty); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -205,34 +236,37 @@ public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRep Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}"); + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Org: {organization.Name}"); Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage) { var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); - var user = Substitute.For(); + var user = Substitute.For(); user.Email = "test@example.com"; user.Name = "Test"; eventMessage.OrganizationId = _organizationId; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); + sutProvider.GetDependency() + .GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()).Returns(user); await sutProvider.Sut.HandleEventAsync(eventMessage); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}"); Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); + await sutProvider.GetDependency().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); } [Theory, BitAutoData] @@ -256,7 +290,7 @@ public async Task HandleEventAsync_FilterReturnsTrue_PublishesIntegrationMessage await sutProvider.Sut.HandleEventAsync(eventMessage); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); @@ -298,7 +332,7 @@ public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_PublishesIn foreach (var eventMessage in eventMessages) { - var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( @@ -316,7 +350,7 @@ public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesI foreach (var eventMessage in eventMessages) { - var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual( diff --git a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs index d9df9486b6d0..aee4af346cb2 100644 --- a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs @@ -83,6 +83,7 @@ public void ReplaceTokens_TemplateIsEmpty_ReturnsOriginalString(EventMessage eve [Theory] [InlineData("User name is #UserName#")] [InlineData("Email: #UserEmail#")] + [InlineData("User type = #UserType#")] public void TemplateRequiresUser_ContainingKeys_ReturnsTrue(string template) { var result = IntegrationTemplateProcessor.TemplateRequiresUser(template); @@ -102,6 +103,7 @@ public void TemplateRequiresUser_EmptyInputOrNoMatchingKeys_ReturnsFalse(string [Theory] [InlineData("Acting user is #ActingUserName#")] [InlineData("Acting user's email is #ActingUserEmail#")] + [InlineData("Acting user's type is #ActingUserType#")] public void TemplateRequiresActingUser_ContainingKeys_ReturnsTrue(string template) { var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template); @@ -118,6 +120,25 @@ public void TemplateRequiresActingUser_EmptyInputOrNoMatchingKeys_ReturnsFalse(s Assert.False(result); } + [Theory] + [InlineData("Group name is #GroupName#!")] + [InlineData("Group: #GroupName#")] + public void TemplateRequiresGroup_ContainingKeys_ReturnsTrue(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresGroup(template); + Assert.True(result); + } + + [Theory] + [InlineData("#GroupId#")] // This is on the base class, not fetched, so should be false + [InlineData("No Group Tokens")] + [InlineData("")] + public void TemplateRequiresGroup_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresGroup(template); + Assert.False(result); + } + [Theory] [InlineData("Organization: #OrganizationName#")] [InlineData("Welcome to #OrganizationName#")] diff --git a/util/Migrator/DbScripts/2025-11-05_00_OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql b/util/Migrator/DbScripts/2025-11-05_00_OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql new file mode 100644 index 000000000000..ea599d0776f8 --- /dev/null +++ b/util/Migrator/DbScripts/2025-11-05_00_OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql @@ -0,0 +1,17 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + TOP 1 * +FROM + [dbo].[OrganizationUserUserDetailsView] +WHERE + [OrganizationId] = @OrganizationId + AND + [UserId] = @UserId +END +go