1
0
mirror of https://github.com/bitwarden/server synced 2025-12-10 05:13:48 +00:00

Add template properties for Datadog (#6528)

* Add template properites for Datadog

* Add test and implementation for including User and ActingUser when only the Type is referenced

* Refactored database calls to fetch the user details in a single DB call

* Refactor to use a dedicated stored procedure for Dapper

* Remove TOP 1 from stored procedure

* Accept Claude's optimization of SingleOrDefaultAsync to unify Dapper/EF

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Revert earlier change and add TOP 1 back into stored procedure

* Change go to GO

* Revert back to version that assumes uniqueness, remove TOP 1

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
Brant DeBow
2025-11-24 10:30:45 -05:00
committed by GitHub
parent fcc879bd23
commit 9573cab37e
12 changed files with 259 additions and 40 deletions

View File

@@ -1,8 +1,8 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
@@ -36,13 +36,18 @@ public class IntegrationTemplateContext(EventMessage eventMessage)
public string DateIso8601 => Date.ToString("o"); public string DateIso8601 => Date.ToString("o");
public string EventMessage => JsonSerializer.Serialize(Event); public string EventMessage => JsonSerializer.Serialize(Event);
public User? User { get; set; } public OrganizationUserUserDetails? User { get; set; }
public string? UserName => User?.Name; public string? UserName => User?.Name;
public string? UserEmail => User?.Email; 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? ActingUserName => ActingUser?.Name;
public string? ActingUserEmail => ActingUser?.Email; 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 Organization? Organization { get; set; }
public string? OrganizationName => Organization?.DisplayName(); public string? OrganizationName => Organization?.DisplayName();

View File

@@ -97,4 +97,15 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
/// <param name="organizationUserToConfirm">Accepted OrganizationUser to confirm</param> /// <param name="organizationUserToConfirm">Accepted OrganizationUser to confirm</param>
/// <returns>True, if the user was updated. False, if not performed.</returns> /// <returns>True, if the user was updated. False, if not performed.</returns>
Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm); Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm);
/// <summary>
/// Returns the OrganizationUserUserDetails if found.
/// </summary>
/// <param name="organizationId">The id of the organization</param>
/// <param name="userId">The id of the User to fetch</param>
/// <returns>OrganizationUserUserDetails of the specified user or null if not found</returns>
/// <remarks>
/// Similar to GetByOrganizationAsync, but returns the user details.
/// </remarks>
Task<OrganizationUserUserDetails?> GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId);
} }

View File

@@ -1,5 +1,6 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities; using Bit.Core.AdminConsole.Utilities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@@ -13,8 +14,9 @@ public class EventIntegrationHandler<T>(
IEventIntegrationPublisher eventIntegrationPublisher, IEventIntegrationPublisher eventIntegrationPublisher,
IIntegrationFilterService integrationFilterService, IIntegrationFilterService integrationFilterService,
IIntegrationConfigurationDetailsCache configurationCache, IIntegrationConfigurationDetailsCache configurationCache,
IUserRepository userRepository, IGroupRepository groupRepository,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ILogger<EventIntegrationHandler<T>> logger) ILogger<EventIntegrationHandler<T>> logger)
: IEventMessageHandler : IEventMessageHandler
{ {
@@ -89,19 +91,35 @@ public class EventIntegrationHandler<T>(
{ {
var context = new IntegrationTemplateContext(eventMessage); 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) 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) 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; return context;

View File

@@ -26,7 +26,7 @@ public static partial class IntegrationTemplateProcessor
return match.Value; // Return unknown keys as keys - i.e. #Key# 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 partial class IntegrationTemplateProcessor
} }
return template.Contains("#UserName#", StringComparison.Ordinal) 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) public static bool TemplateRequiresActingUser(string template)
@@ -49,7 +50,18 @@ public static partial class IntegrationTemplateProcessor
} }
return template.Contains("#ActingUserName#", StringComparison.Ordinal) 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) public static bool TemplateRequiresOrganization(string template)

View File

@@ -688,4 +688,21 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
return rowCount > 0; return rowCount > 0;
} }
public async Task<OrganizationUserUserDetails?> GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var result = await connection.QuerySingleOrDefaultAsync<OrganizationUserUserDetails>(
"[dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId]",
new
{
OrganizationId = organizationId,
UserId = userId
},
commandType: CommandType.StoredProcedure);
return result;
}
}
} }

View File

@@ -965,4 +965,20 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
return true; return true;
} }
#nullable enable
public async Task<OrganizationUserUserDetails?> GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var view = new OrganizationUserUserDetailsViewQuery();
var entity = await view.Run(dbContext).SingleOrDefaultAsync(ou => ou.OrganizationId == organizationId && ou.UserId == userId);
return entity;
}
}
#nullable disable
} }

View File

@@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Models.Teams; using Bit.Core.AdminConsole.Models.Teams;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.AdminConsole.Services.NoopImplementations;
@@ -889,8 +890,9 @@ public static class ServiceCollectionExtensions
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(), eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(), integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(), configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(),
userRepository: provider.GetRequiredService<IUserRepository>(), groupRepository: provider.GetRequiredService<IGroupRepository>(),
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(), organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(),
logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>() logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()
) )
); );
@@ -1016,8 +1018,9 @@ public static class ServiceCollectionExtensions
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(), eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(), integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(), configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(),
userRepository: provider.GetRequiredService<IUserRepository>(), groupRepository: provider.GetRequiredService<IGroupRepository>(),
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(), organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(),
logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>() logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()
) )
); );

View File

@@ -0,0 +1,17 @@
CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId]
@OrganizationId UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationUserUserDetailsView]
WHERE
[OrganizationId] = @OrganizationId
AND
[UserId] = @UserId
END
GO

View File

@@ -2,8 +2,8 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Entities;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Xunit; using Xunit;
@@ -35,7 +35,7 @@ public class IntegrationTemplateContextTests
} }
[Theory, BitAutoData] [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 }; var sut = new IntegrationTemplateContext(eventMessage) { User = user };
@@ -51,7 +51,7 @@ public class IntegrationTemplateContextTests
} }
[Theory, BitAutoData] [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 }; var sut = new IntegrationTemplateContext(eventMessage) { User = user };
@@ -67,7 +67,23 @@ public class IntegrationTemplateContextTests
} }
[Theory, BitAutoData] [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 }; var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
@@ -83,7 +99,7 @@ public class IntegrationTemplateContextTests
} }
[Theory, BitAutoData] [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 }; var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
@@ -98,6 +114,22 @@ public class IntegrationTemplateContextTests
Assert.Null(sut.ActingUserEmail); 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] [Theory, BitAutoData]
public void OrganizationName_WhenOrganizationIsSet_ReturnsDisplayName(EventMessage eventMessage, Organization organization) public void OrganizationName_WhenOrganizationIsSet_ReturnsDisplayName(EventMessage eventMessage, Organization organization)
{ {
@@ -113,4 +145,20 @@ public class IntegrationTemplateContextTests
Assert.Null(sut.OrganizationName); 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);
}
} }

View File

@@ -1,10 +1,11 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Entities; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
@@ -20,9 +21,11 @@ namespace Bit.Core.Test.Services;
public class EventIntegrationHandlerTests public class EventIntegrationHandlerTests
{ {
private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#"; 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 _templateWithOrganization = "Org: #OrganizationName#";
private const string _templateWithUser = "#UserName#, #UserEmail#"; private const string _templateWithUser = "#UserName#, #UserEmail#, #UserType#";
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#, #ActingUserType#";
private static readonly Guid _groupId = Guid.NewGuid();
private static readonly Guid _organizationId = Guid.NewGuid(); private static readonly Guid _organizationId = Guid.NewGuid();
private static readonly Uri _uri = new Uri("https://localhost"); private static readonly Uri _uri = new Uri("https://localhost");
private static readonly Uri _uri2 = new Uri("https://example.com"); private static readonly Uri _uri2 = new Uri("https://example.com");
@@ -45,7 +48,7 @@ public class EventIntegrationHandlerTests
.Create(); .Create();
} }
private static IntegrationMessage<WebhookIntegrationConfigurationDetails> expectedMessage(string template) private static IntegrationMessage<WebhookIntegrationConfigurationDetails> ExpectedMessage(string template)
{ {
return new IntegrationMessage<WebhookIntegrationConfigurationDetails>() return new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
{ {
@@ -105,7 +108,7 @@ public class EventIntegrationHandlerTests
config.Configuration = null; config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = _templateBase; config.Template = _templateBase;
config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup() { }); config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup());
return [config]; return [config];
} }
@@ -138,15 +141,16 @@ public class EventIntegrationHandlerTests
await sutProvider.Sut.HandleEventAsync(eventMessage); await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.expectedMessage( var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
); );
Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>()); await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>()); await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@@ -157,7 +161,7 @@ public class EventIntegrationHandlerTests
await sutProvider.Sut.HandleEventAsync(eventMessage); await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.expectedMessage( var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
); );
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
@@ -167,29 +171,56 @@ public class EventIntegrationHandlerTests
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>()); await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>()); await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage) public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
{ {
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var user = Substitute.For<User>(); var user = Substitute.For<OrganizationUserUserDetails>();
user.Email = "test@example.com"; user.Email = "test@example.com";
user.Name = "Test"; user.Name = "Test";
eventMessage.OrganizationId = _organizationId; eventMessage.OrganizationId = _organizationId;
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user); sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(user);
await sutProvider.Sut.HandleEventAsync(eventMessage); 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()); Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>()); await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty); await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), eventMessage.ActingUserId ?? Guid.Empty);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_GroupTemplate_LoadsGroupFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
var group = Substitute.For<Group>();
group.Name = "Test";
eventMessage.GroupId = _groupId;
eventMessage.OrganizationId = _organizationId;
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(Arg.Any<Guid>()).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<IGroupRepository>().Received(1).GetByIdAsync(eventMessage.GroupId ?? Guid.Empty);
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@@ -205,34 +236,37 @@ public class EventIntegrationHandlerTests
Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}"); var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Org: {organization.Name}");
Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty);
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>()); await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage) public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
{ {
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var user = Substitute.For<User>(); var user = Substitute.For<OrganizationUserUserDetails>();
user.Email = "test@example.com"; user.Email = "test@example.com";
user.Name = "Test"; user.Name = "Test";
eventMessage.OrganizationId = _organizationId; eventMessage.OrganizationId = _organizationId;
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user); sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(user);
await sutProvider.Sut.HandleEventAsync(eventMessage); 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()); Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>()); await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), eventMessage.UserId ?? Guid.Empty);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@@ -256,7 +290,7 @@ public class EventIntegrationHandlerTests
await sutProvider.Sut.HandleEventAsync(eventMessage); await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.expectedMessage( var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
); );
@@ -298,7 +332,7 @@ public class EventIntegrationHandlerTests
foreach (var eventMessage in eventMessages) foreach (var eventMessage in eventMessages)
{ {
var expectedMessage = EventIntegrationHandlerTests.expectedMessage( var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
); );
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
@@ -316,7 +350,7 @@ public class EventIntegrationHandlerTests
foreach (var eventMessage in eventMessages) foreach (var eventMessage in eventMessages)
{ {
var expectedMessage = EventIntegrationHandlerTests.expectedMessage( var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
); );
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual( await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(

View File

@@ -83,6 +83,7 @@ public class IntegrationTemplateProcessorTests
[Theory] [Theory]
[InlineData("User name is #UserName#")] [InlineData("User name is #UserName#")]
[InlineData("Email: #UserEmail#")] [InlineData("Email: #UserEmail#")]
[InlineData("User type = #UserType#")]
public void TemplateRequiresUser_ContainingKeys_ReturnsTrue(string template) public void TemplateRequiresUser_ContainingKeys_ReturnsTrue(string template)
{ {
var result = IntegrationTemplateProcessor.TemplateRequiresUser(template); var result = IntegrationTemplateProcessor.TemplateRequiresUser(template);
@@ -102,6 +103,7 @@ public class IntegrationTemplateProcessorTests
[Theory] [Theory]
[InlineData("Acting user is #ActingUserName#")] [InlineData("Acting user is #ActingUserName#")]
[InlineData("Acting user's email is #ActingUserEmail#")] [InlineData("Acting user's email is #ActingUserEmail#")]
[InlineData("Acting user's type is #ActingUserType#")]
public void TemplateRequiresActingUser_ContainingKeys_ReturnsTrue(string template) public void TemplateRequiresActingUser_ContainingKeys_ReturnsTrue(string template)
{ {
var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template); var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template);
@@ -118,6 +120,25 @@ public class IntegrationTemplateProcessorTests
Assert.False(result); 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] [Theory]
[InlineData("Organization: #OrganizationName#")] [InlineData("Organization: #OrganizationName#")]
[InlineData("Welcome to #OrganizationName#")] [InlineData("Welcome to #OrganizationName#")]

View File

@@ -0,0 +1,17 @@
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId]
@OrganizationId UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationUserUserDetailsView]
WHERE
[OrganizationId] = @OrganizationId
AND
[UserId] = @UserId
END
GO