From 26553e29f9cf5c79191e5e6973492a74dde2ba86 Mon Sep 17 00:00:00 2001 From: Brant DeBow Date: Fri, 31 Oct 2025 14:48:06 -0400 Subject: [PATCH 1/4] Add template properites for Datadog --- .../IntegrationTemplateContext.cs | 11 ++- .../EventIntegrationHandler.cs | 30 +++++-- .../Utilities/IntegrationTemplateProcessor.cs | 10 +++ .../Utilities/ServiceCollectionExtensions.cs | 7 +- .../IntegrationTemplateContextTests.cs | 58 +++++++++++-- .../Services/EventIntegrationHandlerTests.cs | 85 ++++++++++++++----- .../IntegrationTemplateProcessorTests.cs | 19 +++++ 7 files changed, 184 insertions(+), 36 deletions(-) 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/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs index 8423652eb81d..60220310e1f8 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,37 @@ 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); + var orgUser = await organizationUserRepository.GetByOrganizationAsync(organizationId: organizationId, userId: eventMessage.UserId.Value); + if (orgUser is not null) + { + context.User = await organizationUserRepository.GetDetailsByIdAsync(orgUser.Id); + } } if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) { - context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value); + var orgUser = await organizationUserRepository.GetByOrganizationAsync(organizationId: organizationId, userId: eventMessage.ActingUserId.Value); + if (orgUser is not null) + { + context.ActingUser = await organizationUserRepository.GetDetailsByIdAsync(orgUser.Id); + } } - 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..b046f04187bb 100644 --- a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs +++ b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs @@ -52,6 +52,16 @@ public static bool TemplateRequiresActingUser(string template) || template.Contains("#ActingUserEmail#", 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) { if (string.IsNullOrEmpty(template)) diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 75094d1b0a8a..4a330c943df5 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; @@ -895,8 +896,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>>() ) ); @@ -1022,8 +1024,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/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..1f3ece0e33c0 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -1,10 +1,12 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; 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 +22,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 +49,7 @@ private SutProvider expectedMessage(string template) + private static IntegrationMessage ExpectedMessage(string template) { return new IntegrationMessage() { @@ -105,7 +109,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 +142,17 @@ 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().GetByOrganizationAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByIdAsync(Arg.Any()); } [Theory, BitAutoData] @@ -157,7 +163,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 +173,60 @@ 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().GetByOrganizationAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByIdAsync(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() + .GetByOrganizationAsync(Arg.Any(), Arg.Any()).Returns(Substitute.For()); + sutProvider.GetDependency().GetDetailsByIdAsync(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).GetByOrganizationAsync(Arg.Any(), eventMessage.ActingUserId ?? Guid.Empty); + await sutProvider.GetDependency().Received(1).GetDetailsByIdAsync(Arg.Any()); + } + + [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().GetByOrganizationAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByIdAsync(Arg.Any()); } [Theory, BitAutoData] @@ -205,34 +242,40 @@ 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().GetByOrganizationAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByIdAsync(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() + .GetByOrganizationAsync(Arg.Any(), Arg.Any()).Returns(Substitute.For()); + sutProvider.GetDependency().GetDetailsByIdAsync(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).GetByOrganizationAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); + await sutProvider.GetDependency().Received(1).GetDetailsByIdAsync(Arg.Any()); } [Theory, BitAutoData] @@ -256,7 +299,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 +341,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 +359,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..91a34e27a26a 100644 --- a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs @@ -118,6 +118,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#")] From 123cf92d2e7929d8285266102c4911fd7911256c Mon Sep 17 00:00:00 2001 From: Brant DeBow Date: Mon, 3 Nov 2025 08:24:10 -0500 Subject: [PATCH 2/4] Add test and implementation for including User and ActingUser when only the Type is referenced --- .../Utilities/IntegrationTemplateProcessor.cs | 8 +++++--- .../Utilities/IntegrationTemplateProcessorTests.cs | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs index b046f04187bb..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,8 @@ 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) diff --git a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs index 91a34e27a26a..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); From 7a7fc804134ce3a7f0032ee2d8a50ee33ce24cb9 Mon Sep 17 00:00:00 2001 From: Brant DeBow Date: Wed, 5 Nov 2025 08:05:50 -0500 Subject: [PATCH 3/4] Refactored database calls to fetch the user details in a single DB call --- .../IOrganizationUserRepository.cs | 11 ++++++++ .../EventIntegrationHandler.cs | 18 ++++++------- .../OrganizationUserRepository.cs | 13 ++++++++++ .../OrganizationUserRepository.cs | 13 ++++++++++ .../Services/EventIntegrationHandlerTests.cs | 25 ++++++------------- 5 files changed, 53 insertions(+), 27 deletions(-) diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index b17de3c51d3b..d8b73c7627f3 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 GetDetailsByOrganizationUserAsync(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 60220310e1f8..12655ad671f8 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs @@ -103,20 +103,18 @@ private async Task BuildContextAsync(EventMessage ev if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue) { - var orgUser = await organizationUserRepository.GetByOrganizationAsync(organizationId: organizationId, userId: eventMessage.UserId.Value); - if (orgUser is not null) - { - context.User = await organizationUserRepository.GetDetailsByIdAsync(orgUser.Id); - } + context.User = await organizationUserRepository.GetDetailsByOrganizationUserAsync( + organizationId: organizationId, + userId: eventMessage.UserId.Value + ); } if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) { - var orgUser = await organizationUserRepository.GetByOrganizationAsync(organizationId: organizationId, userId: eventMessage.ActingUserId.Value); - if (orgUser is not null) - { - context.ActingUser = await organizationUserRepository.GetDetailsByIdAsync(orgUser.Id); - } + context.ActingUser = await organizationUserRepository.GetDetailsByOrganizationUserAsync( + organizationId: organizationId, + userId: eventMessage.ActingUserId.Value + ); } if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template)) diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index dc4fc74ff8a9..4b128114e710 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -686,4 +686,17 @@ public async Task ConfirmOrganizationUserAsync(OrganizationUser organizati return rowCount > 0; } + + public async Task GetDetailsByOrganizationUserAsync(Guid organizationId, Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationUserUserDetails_ReadByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.FirstOrDefault(u => u.UserId == userId); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index b871ec44bf9d..f53ce3f5286c 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -962,4 +962,17 @@ public async Task ConfirmOrganizationUserAsync(Core.Entities.OrganizationU return true; } + + public async Task GetDetailsByOrganizationUserAsync(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; + } + } + + } diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index 1f3ece0e33c0..131d320a1c39 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -2,7 +2,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; @@ -151,8 +150,7 @@ 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().GetByOrganizationAsync(Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -175,8 +173,7 @@ await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByOrganizationAsync(Arg.Any(), Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -189,8 +186,7 @@ public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(Ev eventMessage.OrganizationId = _organizationId; sutProvider.GetDependency() - .GetByOrganizationAsync(Arg.Any(), Arg.Any()).Returns(Substitute.For()); - sutProvider.GetDependency().GetDetailsByIdAsync(Arg.Any()).Returns(user); + .GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()).Returns(user); await sutProvider.Sut.HandleEventAsync(eventMessage); var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}"); @@ -200,8 +196,7 @@ 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).GetByOrganizationAsync(Arg.Any(), eventMessage.ActingUserId ?? Guid.Empty); - await sutProvider.GetDependency().Received(1).GetDetailsByIdAsync(Arg.Any()); + await sutProvider.GetDependency().Received(1).GetDetailsByOrganizationUserAsync(Arg.Any(), eventMessage.ActingUserId ?? Guid.Empty); } [Theory, BitAutoData] @@ -225,8 +220,7 @@ 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().GetByOrganizationAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -249,8 +243,7 @@ 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().GetByOrganizationAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -263,8 +256,7 @@ public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMes eventMessage.OrganizationId = _organizationId; sutProvider.GetDependency() - .GetByOrganizationAsync(Arg.Any(), Arg.Any()).Returns(Substitute.For()); - sutProvider.GetDependency().GetDetailsByIdAsync(Arg.Any()).Returns(user); + .GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()).Returns(user); await sutProvider.Sut.HandleEventAsync(eventMessage); var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}"); @@ -274,8 +266,7 @@ 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).GetByOrganizationAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); - await sutProvider.GetDependency().Received(1).GetDetailsByIdAsync(Arg.Any()); + await sutProvider.GetDependency().Received(1).GetDetailsByOrganizationUserAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); } [Theory, BitAutoData] From 7148d3663c98ddc5a2d27d5e4fea7e2a9481f0a0 Mon Sep 17 00:00:00 2001 From: Brant DeBow Date: Wed, 5 Nov 2025 17:46:26 -0500 Subject: [PATCH 4/4] Refactor to use a dedicated stored procedure for Dapper --- .../IOrganizationUserRepository.cs | 2 +- .../EventIntegrationHandler.cs | 4 ++-- .../Repositories/OrganizationUserRepository.cs | 14 +++++++++----- .../Repositories/OrganizationUserRepository.cs | 5 ++++- ...rUserDetails_ReadByOrganizationIdUserId.sql | 18 ++++++++++++++++++ .../Services/EventIntegrationHandlerTests.cs | 16 ++++++++-------- ...rUserDetails_ReadByOrganizationIdUserId.sql | 17 +++++++++++++++++ 7 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql create mode 100644 util/Migrator/DbScripts/2025-11-05_00_OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index d8b73c7627f3..142c6855a394 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -106,5 +106,5 @@ UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, /// /// Similar to GetByOrganizationAsync, but returns the user details. /// - Task GetDetailsByOrganizationUserAsync(Guid organizationId, Guid userId); + 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 12655ad671f8..e29d0eaaadf0 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs @@ -103,7 +103,7 @@ private async Task BuildContextAsync(EventMessage ev if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue) { - context.User = await organizationUserRepository.GetDetailsByOrganizationUserAsync( + context.User = await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( organizationId: organizationId, userId: eventMessage.UserId.Value ); @@ -111,7 +111,7 @@ private async Task BuildContextAsync(EventMessage ev if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) { - context.ActingUser = await organizationUserRepository.GetDetailsByOrganizationUserAsync( + context.ActingUser = await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( organizationId: organizationId, userId: eventMessage.ActingUserId.Value ); diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index 4b128114e710..2f2af1322721 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -687,16 +687,20 @@ public async Task ConfirmOrganizationUserAsync(OrganizationUser organizati return rowCount > 0; } - public async Task GetDetailsByOrganizationUserAsync(Guid organizationId, Guid userId) + public async Task GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId) { using (var connection = new SqlConnection(ConnectionString)) { - var results = await connection.QueryAsync( - "[dbo].[OrganizationUserUserDetails_ReadByOrganizationId]", - new { OrganizationId = organizationId }, + var result = await connection.QuerySingleOrDefaultAsync( + "[dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId]", + new + { + OrganizationId = organizationId, + UserId = userId + }, commandType: CommandType.StoredProcedure); - return results.FirstOrDefault(u => u.UserId == userId); + return result; } } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index f53ce3f5286c..e034648cd21f 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -963,7 +963,9 @@ public async Task ConfirmOrganizationUserAsync(Core.Entities.OrganizationU } - public async Task GetDetailsByOrganizationUserAsync(Guid organizationId, Guid userId) +#nullable enable + + public async Task GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId) { using (var scope = ServiceScopeFactory.CreateScope()) { @@ -973,6 +975,7 @@ public async Task GetDetailsByOrganizationUserAsync return entity; } } +#nullable disable } 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/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index 131d320a1c39..c556c1fae0b8 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -150,7 +150,7 @@ 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().GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -173,7 +173,7 @@ await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -186,7 +186,7 @@ public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(Ev eventMessage.OrganizationId = _organizationId; sutProvider.GetDependency() - .GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()).Returns(user); + .GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()).Returns(user); await sutProvider.Sut.HandleEventAsync(eventMessage); var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}"); @@ -196,7 +196,7 @@ 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).GetDetailsByOrganizationUserAsync(Arg.Any(), eventMessage.ActingUserId ?? Guid.Empty); + await sutProvider.GetDependency().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), eventMessage.ActingUserId ?? Guid.Empty); } [Theory, BitAutoData] @@ -220,7 +220,7 @@ 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().GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -243,7 +243,7 @@ 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().GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -256,7 +256,7 @@ public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMes eventMessage.OrganizationId = _organizationId; sutProvider.GetDependency() - .GetDetailsByOrganizationUserAsync(Arg.Any(), Arg.Any()).Returns(user); + .GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()).Returns(user); await sutProvider.Sut.HandleEventAsync(eventMessage); var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}"); @@ -266,7 +266,7 @@ 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).GetDetailsByOrganizationUserAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); + await sutProvider.GetDependency().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); } [Theory, BitAutoData] 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