mirror of
https://github.com/bitwarden/server
synced 2025-12-18 17:23:28 +00:00
Use extended cache for caching integration configuration details (#6650)
* Use extended cache for caching integration configuration details * Alter strategy to use one cache / database call to retrieve all configurations for an event (including wildcards) * Renamed migration per @withinfocus suggestion
This commit is contained in:
@@ -2,8 +2,6 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Entities;
|
namespace Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
public class OrganizationIntegration : ITableObject<Guid>
|
public class OrganizationIntegration : ITableObject<Guid>
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Entities;
|
namespace Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
public class OrganizationIntegrationConfiguration : ITableObject<Guid>
|
public class OrganizationIntegrationConfiguration : ITableObject<Guid>
|
||||||
|
|||||||
@@ -6,10 +6,23 @@ namespace Bit.Core.Repositories;
|
|||||||
|
|
||||||
public interface IOrganizationIntegrationConfigurationRepository : IRepository<OrganizationIntegrationConfiguration, Guid>
|
public interface IOrganizationIntegrationConfigurationRepository : IRepository<OrganizationIntegrationConfiguration, Guid>
|
||||||
{
|
{
|
||||||
Task<List<OrganizationIntegrationConfigurationDetails>> GetConfigurationDetailsAsync(
|
/// <summary>
|
||||||
|
/// Retrieve the list of available configuration details for a specific event for the organization and
|
||||||
|
/// integration type.<br/>
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Note:</b> This returns all configurations that match the event type explicitly <b>and</b>
|
||||||
|
/// all the configurations that have a null event type - null event type is considered a
|
||||||
|
/// wildcard that matches all events.
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="eventType">The specific event type</param>
|
||||||
|
/// <param name="organizationId">The id of the organization</param>
|
||||||
|
/// <param name="integrationType">The integration type</param>
|
||||||
|
/// <returns>A List of <see cref="OrganizationIntegrationConfigurationDetails"/> that match</returns>
|
||||||
|
Task<List<OrganizationIntegrationConfigurationDetails>> GetManyByEventTypeOrganizationIdIntegrationType(
|
||||||
|
EventType eventType,
|
||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
IntegrationType integrationType,
|
IntegrationType integrationType);
|
||||||
EventType eventType);
|
|
||||||
|
|
||||||
Task<List<OrganizationIntegrationConfigurationDetails>> GetAllConfigurationDetailsAsync();
|
Task<List<OrganizationIntegrationConfigurationDetails>> GetAllConfigurationDetailsAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@@ -17,8 +18,8 @@ public class EventIntegrationHandler<T>(
|
|||||||
IntegrationType integrationType,
|
IntegrationType integrationType,
|
||||||
IEventIntegrationPublisher eventIntegrationPublisher,
|
IEventIntegrationPublisher eventIntegrationPublisher,
|
||||||
IIntegrationFilterService integrationFilterService,
|
IIntegrationFilterService integrationFilterService,
|
||||||
IIntegrationConfigurationDetailsCache configurationCache,
|
|
||||||
IFusionCache cache,
|
IFusionCache cache,
|
||||||
|
IOrganizationIntegrationConfigurationRepository configurationRepository,
|
||||||
IGroupRepository groupRepository,
|
IGroupRepository groupRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
@@ -27,17 +28,7 @@ public class EventIntegrationHandler<T>(
|
|||||||
{
|
{
|
||||||
public async Task HandleEventAsync(EventMessage eventMessage)
|
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
if (eventMessage.OrganizationId is not Guid organizationId)
|
foreach (var configuration in await GetConfigurationDetailsListAsync(eventMessage))
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var configurations = configurationCache.GetConfigurationDetails(
|
|
||||||
organizationId,
|
|
||||||
integrationType,
|
|
||||||
eventMessage.Type);
|
|
||||||
|
|
||||||
foreach (var configuration in configurations)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -64,7 +55,7 @@ public class EventIntegrationHandler<T>(
|
|||||||
{
|
{
|
||||||
IntegrationType = integrationType,
|
IntegrationType = integrationType,
|
||||||
MessageId = messageId.ToString(),
|
MessageId = messageId.ToString(),
|
||||||
OrganizationId = organizationId.ToString(),
|
OrganizationId = eventMessage.OrganizationId?.ToString(),
|
||||||
Configuration = config,
|
Configuration = config,
|
||||||
RenderedTemplate = renderedTemplate,
|
RenderedTemplate = renderedTemplate,
|
||||||
RetryCount = 0,
|
RetryCount = 0,
|
||||||
@@ -132,6 +123,37 @@ public class EventIntegrationHandler<T>(
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<List<OrganizationIntegrationConfigurationDetails>> GetConfigurationDetailsListAsync(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
if (eventMessage.OrganizationId is not Guid organizationId)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OrganizationIntegrationConfigurationDetails> configurations = [];
|
||||||
|
|
||||||
|
var integrationTag = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
organizationId,
|
||||||
|
integrationType
|
||||||
|
);
|
||||||
|
|
||||||
|
configurations.AddRange(await cache.GetOrSetAsync<List<OrganizationIntegrationConfigurationDetails>>(
|
||||||
|
key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
organizationId: organizationId,
|
||||||
|
integrationType: integrationType,
|
||||||
|
eventType: eventMessage.Type),
|
||||||
|
factory: async _ => await configurationRepository.GetManyByEventTypeOrganizationIdIntegrationType(
|
||||||
|
eventType: eventMessage.Type,
|
||||||
|
organizationId: organizationId,
|
||||||
|
integrationType: integrationType),
|
||||||
|
options: new FusionCacheEntryOptions(
|
||||||
|
duration: EventIntegrationsCacheConstants.DurationForOrganizationIntegrationConfigurationDetails),
|
||||||
|
tags: [integrationTag]
|
||||||
|
));
|
||||||
|
|
||||||
|
return configurations;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<OrganizationUserUserDetails?> GetUserFromCacheAsync(Guid organizationId, Guid userId) =>
|
private async Task<OrganizationUserUserDetails?> GetUserFromCacheAsync(Guid organizationId, Guid userId) =>
|
||||||
await cache.GetOrSetAsync<OrganizationUserUserDetails?>(
|
await cache.GetOrSetAsync<OrganizationUserUserDetails?>(
|
||||||
key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(organizationId, userId),
|
key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(organizationId, userId),
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data.Organizations;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Settings;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
|
||||||
|
|
||||||
public class IntegrationConfigurationDetailsCacheService : BackgroundService, IIntegrationConfigurationDetailsCache
|
|
||||||
{
|
|
||||||
private readonly record struct IntegrationCacheKey(Guid OrganizationId, IntegrationType IntegrationType, EventType? EventType);
|
|
||||||
private readonly IOrganizationIntegrationConfigurationRepository _repository;
|
|
||||||
private readonly ILogger<IntegrationConfigurationDetailsCacheService> _logger;
|
|
||||||
private readonly TimeSpan _refreshInterval;
|
|
||||||
private Dictionary<IntegrationCacheKey, List<OrganizationIntegrationConfigurationDetails>> _cache = new();
|
|
||||||
|
|
||||||
public IntegrationConfigurationDetailsCacheService(
|
|
||||||
IOrganizationIntegrationConfigurationRepository repository,
|
|
||||||
GlobalSettings globalSettings,
|
|
||||||
ILogger<IntegrationConfigurationDetailsCacheService> logger)
|
|
||||||
{
|
|
||||||
_repository = repository;
|
|
||||||
_logger = logger;
|
|
||||||
_refreshInterval = TimeSpan.FromMinutes(globalSettings.EventLogging.IntegrationCacheRefreshIntervalMinutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<OrganizationIntegrationConfigurationDetails> GetConfigurationDetails(
|
|
||||||
Guid organizationId,
|
|
||||||
IntegrationType integrationType,
|
|
||||||
EventType eventType)
|
|
||||||
{
|
|
||||||
var specificKey = new IntegrationCacheKey(organizationId, integrationType, eventType);
|
|
||||||
var allEventsKey = new IntegrationCacheKey(organizationId, integrationType, null);
|
|
||||||
|
|
||||||
var results = new List<OrganizationIntegrationConfigurationDetails>();
|
|
||||||
|
|
||||||
if (_cache.TryGetValue(specificKey, out var specificConfigs))
|
|
||||||
{
|
|
||||||
results.AddRange(specificConfigs);
|
|
||||||
}
|
|
||||||
if (_cache.TryGetValue(allEventsKey, out var fallbackConfigs))
|
|
||||||
{
|
|
||||||
results.AddRange(fallbackConfigs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
await RefreshAsync();
|
|
||||||
|
|
||||||
var timer = new PeriodicTimer(_refreshInterval);
|
|
||||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
|
||||||
{
|
|
||||||
await RefreshAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task RefreshAsync()
|
|
||||||
{
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var newCache = (await _repository.GetAllConfigurationDetailsAsync())
|
|
||||||
.GroupBy(x => new IntegrationCacheKey(x.OrganizationId, x.IntegrationType, x.EventType))
|
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
|
||||||
_cache = newCache;
|
|
||||||
|
|
||||||
stopwatch.Stop();
|
|
||||||
_logger.LogInformation(
|
|
||||||
"[IntegrationConfigurationDetailsCacheService] Refreshed successfully: {Count} entries in {Duration}ms",
|
|
||||||
newCache.Count,
|
|
||||||
stopwatch.Elapsed.TotalMilliseconds);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError("[IntegrationConfigurationDetailsCacheService] Refresh failed: {ex}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -295,33 +295,59 @@ graph TD
|
|||||||
```
|
```
|
||||||
## Caching
|
## Caching
|
||||||
|
|
||||||
To reduce database load and improve performance, integration configurations are cached in-memory as a Dictionary
|
To reduce database load and improve performance, event integrations uses its own named extended cache (see
|
||||||
with a periodic load of all configurations. Without caching, each incoming `EventMessage` would trigger a database
|
the [README in Utilities](https://github.com/bitwarden/server/blob/main/src/Core/Utilities/README.md#extended-cache)
|
||||||
|
for more information). Without caching, for instance, each incoming `EventMessage` would trigger a database
|
||||||
query to retrieve the relevant `OrganizationIntegrationConfigurationDetails`.
|
query to retrieve the relevant `OrganizationIntegrationConfigurationDetails`.
|
||||||
|
|
||||||
By loading all configurations into memory on a fixed interval, we ensure:
|
### `EventIntegrationsCacheConstants`
|
||||||
|
|
||||||
- Consistent performance for reads.
|
`EventIntegrationsCacheConstants` allows the code to have strongly typed references to a number of cache-related
|
||||||
- Reduced database pressure.
|
details when working with the extended cache. The cache name and all cache keys and tags are programmatically accessed
|
||||||
- Predictable refresh timing, independent of event activity.
|
from `EventIntegrationsCacheConstants` rather than simple strings. For instance,
|
||||||
|
`EventIntegrationsCacheConstants.CacheName` is used in the cache setup, keyed services, dependency injection, etc.,
|
||||||
|
rather than using a string literal (i.e. "EventIntegrations") in code.
|
||||||
|
|
||||||
### Architecture / Design
|
### `OrganizationIntegrationConfigurationDetails`
|
||||||
|
|
||||||
- The cache is read-only for consumers. It is only updated in bulk by a background refresh process.
|
- This is one of the most actively used portions of the architecture because any event that has an associated
|
||||||
- The cache is fully replaced on each refresh to avoid locking or partial state.
|
organization requires a check of the configurations to determine if we need to fire off an integration.
|
||||||
|
- By using the extended cache, all reads are hitting the L1 or L2 cache before needing to access the database.
|
||||||
- Reads return a `List<OrganizationIntegrationConfigurationDetails>` for a given key or an empty list if no
|
- Reads return a `List<OrganizationIntegrationConfigurationDetails>` for a given key or an empty list if no
|
||||||
match exists.
|
match exists.
|
||||||
- Failures or delays in the loading process do not affect the existing cache state. The cache will continue serving
|
- The TTL is set very high on these records (1 day). This is because when the admin API makes any changes, it
|
||||||
the last known good state until the update replaces the whole cache.
|
tells the cache to remove that key. This propagates to the event listening code via the extended cache backplane,
|
||||||
|
which means that the cache is then expired and the next read will fetch the new values. This allows us to have
|
||||||
|
a high TTL and avoid needing to refresh values except when necessary.
|
||||||
|
|
||||||
### Background Refresh
|
#### Tagging per integration
|
||||||
|
|
||||||
A hosted service (`IntegrationConfigurationDetailsCacheService`) runs in the background and:
|
- Each entry in the cache (which again, returns `List<OrganizationIntegrationConfigurationDetails>`) is tagged with
|
||||||
|
the organization id and the integration type.
|
||||||
|
- This allows us to remove all of a given organization's configuration details for an integration when the admin
|
||||||
|
makes changes at the integration level.
|
||||||
|
- For instance, if there were 5 events configured for a given organization's webhook and the admin changed the URL
|
||||||
|
at the integration level, the updates would need to be propagated or else the cache will continue returning the
|
||||||
|
stale URL.
|
||||||
|
- By tagging each of the entries, the API can ask the extended cache to remove all the entries for a given
|
||||||
|
organization integration in one call. The cache will handle dropping / refreshing these entries in a
|
||||||
|
performant way.
|
||||||
|
- There are two places in the code that are both aware of the tagging functionality
|
||||||
|
- The `EventIntegrationHandler` must use the tag when fetching relevant configuration details. This tells the cache
|
||||||
|
to store the entry with the tag when it successfully loads from the repository.
|
||||||
|
- The `OrganizationIntegrationController` needs to use the tag to remove all the tagged entries when and admin
|
||||||
|
creates, updates, or deletes an integration.
|
||||||
|
- To ensure both places are synchronized on how to tag entries, they both use
|
||||||
|
`EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration` to build the tag.
|
||||||
|
|
||||||
- Loads all configuration records at application startup.
|
### Template Properties
|
||||||
- Refreshes the cache on a configurable interval.
|
|
||||||
- Logs timing and entry count on success.
|
- The `IntegrationTemplateProcessor` supports some properties that require an additional lookup. For instance,
|
||||||
- Logs exceptions on failure without disrupting application flow.
|
the `UserId` is provided as part of the `EventMessage`, but `UserName` means an additional lookup to map the user
|
||||||
|
id to the actual name.
|
||||||
|
- The properties for a `User` (which includes `ActingUser`), `Group`, and `Organization` are cached via the
|
||||||
|
extended cache with a default TTL of 30 minutes.
|
||||||
|
- This is cached in both the L1 (Memory) and L2 (Redis) and will be automatically refreshed as needed.
|
||||||
|
|
||||||
# Building a new integration
|
# Building a new integration
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
namespace Bit.Core.Utilities;
|
namespace Bit.Core.Utilities;
|
||||||
@@ -11,7 +13,12 @@ public static class EventIntegrationsCacheConstants
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base cache name used for storing event integration data.
|
/// The base cache name used for storing event integration data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly string CacheName = "EventIntegrations";
|
public const string CacheName = "EventIntegrations";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Duration TimeSpan for adding OrganizationIntegrationConfigurationDetails to the cache.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly TimeSpan DurationForOrganizationIntegrationConfigurationDetails = TimeSpan.FromDays(1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a deterministic cache key for a <see cref="Group"/>.
|
/// Builds a deterministic cache key for a <see cref="Group"/>.
|
||||||
@@ -20,10 +27,8 @@ public static class EventIntegrationsCacheConstants
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// A cache key for this Group.
|
/// A cache key for this Group.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public static string BuildCacheKeyForGroup(Guid groupId)
|
public static string BuildCacheKeyForGroup(Guid groupId) =>
|
||||||
{
|
$"Group:{groupId:N}";
|
||||||
return $"Group:{groupId:N}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a deterministic cache key for an <see cref="Organization"/>.
|
/// Builds a deterministic cache key for an <see cref="Organization"/>.
|
||||||
@@ -32,10 +37,8 @@ public static class EventIntegrationsCacheConstants
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// A cache key for the Organization.
|
/// A cache key for the Organization.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public static string BuildCacheKeyForOrganization(Guid organizationId)
|
public static string BuildCacheKeyForOrganization(Guid organizationId) =>
|
||||||
{
|
$"Organization:{organizationId:N}";
|
||||||
return $"Organization:{organizationId:N}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a deterministic cache key for an organization user <see cref="OrganizationUserUserDetails"/>.
|
/// Builds a deterministic cache key for an organization user <see cref="OrganizationUserUserDetails"/>.
|
||||||
@@ -45,8 +48,37 @@ public static class EventIntegrationsCacheConstants
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// A cache key for the user.
|
/// A cache key for the user.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public static string BuildCacheKeyForOrganizationUser(Guid organizationId, Guid userId)
|
public static string BuildCacheKeyForOrganizationUser(Guid organizationId, Guid userId) =>
|
||||||
{
|
$"OrganizationUserUserDetails:{organizationId:N}:{userId:N}";
|
||||||
return $"OrganizationUserUserDetails:{organizationId:N}:{userId:N}";
|
|
||||||
}
|
/// <summary>
|
||||||
|
/// Builds a deterministic cache key for an organization's integration configuration details
|
||||||
|
/// <see cref="OrganizationIntegrationConfigurationDetails"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The unique identifier of the organization to which the user belongs.</param>
|
||||||
|
/// <param name="integrationType">The <see cref="IntegrationType"/> of the integration.</param>
|
||||||
|
/// <param name="eventType">The <see cref="EventType"/> of the event configured. Can be null to apply to all events.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A cache key for the configuration details.
|
||||||
|
/// </returns>
|
||||||
|
public static string BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
Guid organizationId,
|
||||||
|
IntegrationType integrationType,
|
||||||
|
EventType? eventType
|
||||||
|
) => $"OrganizationIntegrationConfigurationDetails:{organizationId:N}:{integrationType}:{eventType}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a deterministic tag for tagging an organization's integration configuration details. This tag is then
|
||||||
|
/// used to tag all of the <see cref="OrganizationIntegrationConfigurationDetails"/> that result from this
|
||||||
|
/// integration, which allows us to remove all relevant entries when an integration is changed or removed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The unique identifier of the organization to which the user belongs.</param>
|
||||||
|
/// <param name="integrationType">The <see cref="IntegrationType"/> of the integration.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A cache tag to use for the configuration details.
|
||||||
|
/// </returns>
|
||||||
|
public static string BuildCacheTagForOrganizationIntegration(
|
||||||
|
Guid organizationId,
|
||||||
|
IntegrationType integrationType
|
||||||
|
) => $"OrganizationIntegration:{organizationId:N}:{integrationType}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ public class OrganizationIntegrationConfigurationRepository : Repository<Organiz
|
|||||||
: base(connectionString, readOnlyConnectionString)
|
: base(connectionString, readOnlyConnectionString)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public async Task<List<OrganizationIntegrationConfigurationDetails>> GetConfigurationDetailsAsync(
|
public async Task<List<OrganizationIntegrationConfigurationDetails>>
|
||||||
Guid organizationId,
|
GetManyByEventTypeOrganizationIdIntegrationType(EventType eventType, Guid organizationId,
|
||||||
IntegrationType integrationType,
|
IntegrationType integrationType)
|
||||||
EventType eventType)
|
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(ConnectionString))
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,16 +17,17 @@ public class OrganizationIntegrationConfigurationRepository : Repository<Core.Ad
|
|||||||
: base(serviceScopeFactory, mapper, context => context.OrganizationIntegrationConfigurations)
|
: base(serviceScopeFactory, mapper, context => context.OrganizationIntegrationConfigurations)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public async Task<List<OrganizationIntegrationConfigurationDetails>> GetConfigurationDetailsAsync(
|
public async Task<List<OrganizationIntegrationConfigurationDetails>>
|
||||||
Guid organizationId,
|
GetManyByEventTypeOrganizationIdIntegrationType(EventType eventType, Guid organizationId,
|
||||||
IntegrationType integrationType,
|
IntegrationType integrationType)
|
||||||
EventType eventType)
|
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
{
|
{
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var query = new OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(
|
var query = new OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(
|
||||||
organizationId, eventType, integrationType
|
organizationId,
|
||||||
|
eventType,
|
||||||
|
integrationType
|
||||||
);
|
);
|
||||||
return await query.Run(dbContext).ToListAsync();
|
return await query.Run(dbContext).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,21 @@
|
|||||||
#nullable enable
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||||
|
|
||||||
public class OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery : IQuery<OrganizationIntegrationConfigurationDetails>
|
public class OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(
|
||||||
|
Guid organizationId,
|
||||||
|
EventType eventType,
|
||||||
|
IntegrationType integrationType)
|
||||||
|
: IQuery<OrganizationIntegrationConfigurationDetails>
|
||||||
{
|
{
|
||||||
private readonly Guid _organizationId;
|
|
||||||
private readonly EventType _eventType;
|
|
||||||
private readonly IntegrationType _integrationType;
|
|
||||||
|
|
||||||
public OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(Guid organizationId, EventType eventType, IntegrationType integrationType)
|
|
||||||
{
|
|
||||||
_organizationId = organizationId;
|
|
||||||
_eventType = eventType;
|
|
||||||
_integrationType = integrationType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IQueryable<OrganizationIntegrationConfigurationDetails> Run(DatabaseContext dbContext)
|
public IQueryable<OrganizationIntegrationConfigurationDetails> Run(DatabaseContext dbContext)
|
||||||
{
|
{
|
||||||
var query = from oic in dbContext.OrganizationIntegrationConfigurations
|
var query = from oic in dbContext.OrganizationIntegrationConfigurations
|
||||||
join oi in dbContext.OrganizationIntegrations on oic.OrganizationIntegrationId equals oi.Id into oioic
|
join oi in dbContext.OrganizationIntegrations on oic.OrganizationIntegrationId equals oi.Id
|
||||||
from oi in dbContext.OrganizationIntegrations
|
where oi.OrganizationId == organizationId &&
|
||||||
where oi.OrganizationId == _organizationId &&
|
oi.Type == integrationType &&
|
||||||
oi.Type == _integrationType &&
|
(oic.EventType == eventType || oic.EventType == null)
|
||||||
oic.EventType == _eventType
|
|
||||||
select new OrganizationIntegrationConfigurationDetails()
|
select new OrganizationIntegrationConfigurationDetails()
|
||||||
{
|
{
|
||||||
Id = oic.Id,
|
Id = oic.Id,
|
||||||
|
|||||||
@@ -893,13 +893,11 @@ public static class ServiceCollectionExtensions
|
|||||||
integrationType: listenerConfiguration.IntegrationType,
|
integrationType: listenerConfiguration.IntegrationType,
|
||||||
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
|
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
|
||||||
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
|
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
|
||||||
configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(),
|
|
||||||
cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),
|
cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),
|
||||||
|
configurationRepository: provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
||||||
groupRepository: provider.GetRequiredService<IGroupRepository>(),
|
groupRepository: provider.GetRequiredService<IGroupRepository>(),
|
||||||
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
|
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
|
||||||
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(),
|
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(), logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>())
|
||||||
logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
|
||||||
AzureServiceBusEventListenerService<TListenerConfig>>(provider =>
|
AzureServiceBusEventListenerService<TListenerConfig>>(provider =>
|
||||||
@@ -941,10 +939,6 @@ public static class ServiceCollectionExtensions
|
|||||||
// Add common services
|
// Add common services
|
||||||
services.AddDistributedCache(globalSettings);
|
services.AddDistributedCache(globalSettings);
|
||||||
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
|
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
|
||||||
services.TryAddSingleton<IntegrationConfigurationDetailsCacheService>();
|
|
||||||
services.TryAddSingleton<IIntegrationConfigurationDetailsCache>(provider =>
|
|
||||||
provider.GetRequiredService<IntegrationConfigurationDetailsCacheService>());
|
|
||||||
services.AddHostedService(provider => provider.GetRequiredService<IntegrationConfigurationDetailsCacheService>());
|
|
||||||
services.TryAddSingleton<IIntegrationFilterService, IntegrationFilterService>();
|
services.TryAddSingleton<IIntegrationFilterService, IntegrationFilterService>();
|
||||||
services.TryAddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
services.TryAddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
||||||
|
|
||||||
@@ -1024,13 +1018,11 @@ public static class ServiceCollectionExtensions
|
|||||||
integrationType: listenerConfiguration.IntegrationType,
|
integrationType: listenerConfiguration.IntegrationType,
|
||||||
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
|
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
|
||||||
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
|
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
|
||||||
configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(),
|
|
||||||
cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),
|
cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),
|
||||||
|
configurationRepository: provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
||||||
groupRepository: provider.GetRequiredService<IGroupRepository>(),
|
groupRepository: provider.GetRequiredService<IGroupRepository>(),
|
||||||
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
|
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
|
||||||
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(),
|
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(), logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>())
|
||||||
logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
|
||||||
RabbitMqEventListenerService<TListenerConfig>>(provider =>
|
RabbitMqEventListenerService<TListenerConfig>>(provider =>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ BEGIN
|
|||||||
FROM
|
FROM
|
||||||
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
|
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
|
||||||
WHERE
|
WHERE
|
||||||
oic.[EventType] = @EventType
|
(oic.[EventType] = @EventType OR oic.[EventType] IS NULL)
|
||||||
AND
|
AND
|
||||||
oic.[OrganizationId] = @OrganizationId
|
oic.[OrganizationId] = @OrganizationId
|
||||||
AND
|
AND
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.Text.Json;
|
#nullable enable
|
||||||
|
|
||||||
|
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.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@@ -8,6 +10,7 @@ using Bit.Core.Models.Data.Organizations;
|
|||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
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.Core.Utilities;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
@@ -36,12 +39,16 @@ public class EventIntegrationHandlerTests
|
|||||||
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
|
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
|
||||||
List<OrganizationIntegrationConfigurationDetails> configurations)
|
List<OrganizationIntegrationConfigurationDetails> configurations)
|
||||||
{
|
{
|
||||||
var configurationCache = Substitute.For<IIntegrationConfigurationDetailsCache>();
|
var cache = Substitute.For<IFusionCache>();
|
||||||
configurationCache.GetConfigurationDetails(Arg.Any<Guid>(),
|
cache.GetOrSetAsync(
|
||||||
IntegrationType.Webhook, Arg.Any<EventType>()).Returns(configurations);
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Any<Func<object, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
|
||||||
|
options: Arg.Any<FusionCacheEntryOptions>(),
|
||||||
|
tags: Arg.Any<IEnumerable<string>>()
|
||||||
|
).Returns(configurations);
|
||||||
|
|
||||||
return new SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>()
|
return new SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>()
|
||||||
.SetDependency(configurationCache)
|
.SetDependency(cache)
|
||||||
.SetDependency(_eventIntegrationPublisher)
|
.SetDependency(_eventIntegrationPublisher)
|
||||||
.SetDependency(IntegrationType.Webhook)
|
.SetDependency(IntegrationType.Webhook)
|
||||||
.SetDependency(_logger)
|
.SetDependency(_logger)
|
||||||
@@ -173,6 +180,37 @@ public class EventIntegrationHandlerTests
|
|||||||
Assert.Null(context.ActingUser);
|
Assert.Null(context.ActingUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task BuildContextAsync_ActingUserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||||
|
eventMessage.ActingUserId ??= Guid.NewGuid();
|
||||||
|
organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync(
|
||||||
|
eventMessage.OrganizationId.Value,
|
||||||
|
eventMessage.ActingUserId.Value).Returns(actingUser);
|
||||||
|
|
||||||
|
// Capture the factory function passed to the cache
|
||||||
|
Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>? capturedFactory = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>(f => capturedFactory = f)
|
||||||
|
).Returns(actingUser);
|
||||||
|
|
||||||
|
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
|
||||||
|
|
||||||
|
Assert.NotNull(capturedFactory);
|
||||||
|
var result = await capturedFactory(null!, CancellationToken.None);
|
||||||
|
|
||||||
|
await organizationUserRepository.Received(1).GetDetailsByOrganizationIdUserIdAsync(
|
||||||
|
eventMessage.OrganizationId.Value,
|
||||||
|
eventMessage.ActingUserId.Value);
|
||||||
|
Assert.Equal(actingUser, result);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task BuildContextAsync_GroupIdPresent_UsesCache(EventMessage eventMessage, Group group)
|
public async Task BuildContextAsync_GroupIdPresent_UsesCache(EventMessage eventMessage, Group group)
|
||||||
{
|
{
|
||||||
@@ -211,6 +249,32 @@ public class EventIntegrationHandlerTests
|
|||||||
Assert.Null(context.Group);
|
Assert.Null(context.Group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task BuildContextAsync_GroupFactory_CallsGroupRepository(EventMessage eventMessage, Group group)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
var groupRepository = sutProvider.GetDependency<IGroupRepository>();
|
||||||
|
|
||||||
|
eventMessage.GroupId ??= Guid.NewGuid();
|
||||||
|
groupRepository.GetByIdAsync(eventMessage.GroupId.Value).Returns(group);
|
||||||
|
|
||||||
|
// Capture the factory function passed to the cache
|
||||||
|
Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>? capturedFactory = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>(f => capturedFactory = f)
|
||||||
|
).Returns(group);
|
||||||
|
|
||||||
|
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
|
||||||
|
|
||||||
|
Assert.NotNull(capturedFactory);
|
||||||
|
var result = await capturedFactory(null!, CancellationToken.None);
|
||||||
|
|
||||||
|
await groupRepository.Received(1).GetByIdAsync(eventMessage.GroupId.Value);
|
||||||
|
Assert.Equal(group, result);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task BuildContextAsync_OrganizationIdPresent_UsesCache(EventMessage eventMessage, Organization organization)
|
public async Task BuildContextAsync_OrganizationIdPresent_UsesCache(EventMessage eventMessage, Organization organization)
|
||||||
{
|
{
|
||||||
@@ -250,6 +314,32 @@ public class EventIntegrationHandlerTests
|
|||||||
Assert.Null(context.Organization);
|
Assert.Null(context.Organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task BuildContextAsync_OrganizationFactory_CallsOrganizationRepository(EventMessage eventMessage, Organization organization)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
|
||||||
|
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||||
|
organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value).Returns(organization);
|
||||||
|
|
||||||
|
// Capture the factory function passed to the cache
|
||||||
|
Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>? capturedFactory = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>(f => capturedFactory = f)
|
||||||
|
).Returns(organization);
|
||||||
|
|
||||||
|
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
|
||||||
|
|
||||||
|
Assert.NotNull(capturedFactory);
|
||||||
|
var result = await capturedFactory(null!, CancellationToken.None);
|
||||||
|
|
||||||
|
await organizationRepository.Received(1).GetByIdAsync(eventMessage.OrganizationId.Value);
|
||||||
|
Assert.Equal(organization, result);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task BuildContextAsync_UserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
|
public async Task BuildContextAsync_UserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
|
||||||
{
|
{
|
||||||
@@ -313,6 +403,38 @@ public class EventIntegrationHandlerTests
|
|||||||
Assert.Null(context.User);
|
Assert.Null(context.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task BuildContextAsync_UserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||||
|
eventMessage.UserId ??= Guid.NewGuid();
|
||||||
|
organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync(
|
||||||
|
eventMessage.OrganizationId.Value,
|
||||||
|
eventMessage.UserId.Value).Returns(userDetails);
|
||||||
|
|
||||||
|
// Capture the factory function passed to the cache
|
||||||
|
Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>? capturedFactory = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>(f => capturedFactory = f)
|
||||||
|
).Returns(userDetails);
|
||||||
|
|
||||||
|
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
|
||||||
|
|
||||||
|
Assert.NotNull(capturedFactory);
|
||||||
|
var result = await capturedFactory(null!, CancellationToken.None);
|
||||||
|
|
||||||
|
await organizationUserRepository.Received(1).GetDetailsByOrganizationIdUserIdAsync(
|
||||||
|
eventMessage.OrganizationId.Value,
|
||||||
|
eventMessage.UserId.Value);
|
||||||
|
Assert.Equal(userDetails, result);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task BuildContextAsync_NoSpecialTokens_DoesNotCallAnyCache(EventMessage eventMessage)
|
public async Task BuildContextAsync_NoSpecialTokens_DoesNotCallAnyCache(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
@@ -344,6 +466,12 @@ public class EventIntegrationHandlerTests
|
|||||||
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
|
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider(NoConfigurations());
|
var sutProvider = GetSutProvider(NoConfigurations());
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
cache.GetOrSetAsync<List<OrganizationIntegrationConfigurationDetails>>(
|
||||||
|
Arg.Any<string>(),
|
||||||
|
Arg.Any<Func<object, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
|
||||||
|
Arg.Any<FusionCacheEntryOptions>()
|
||||||
|
).Returns(NoConfigurations());
|
||||||
|
|
||||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
|
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
|
||||||
@@ -362,8 +490,8 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)
|
public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
|
||||||
eventMessage.OrganizationId = _organizationId;
|
eventMessage.OrganizationId = _organizationId;
|
||||||
|
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
||||||
|
|
||||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
@@ -382,8 +510,8 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage)
|
public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
|
||||||
eventMessage.OrganizationId = _organizationId;
|
eventMessage.OrganizationId = _organizationId;
|
||||||
|
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
||||||
|
|
||||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
@@ -405,6 +533,7 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage)
|
public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
||||||
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
||||||
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(false);
|
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(false);
|
||||||
@@ -416,10 +545,10 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_FilterReturnsTrue_PublishesIntegrationMessage(EventMessage eventMessage)
|
public async Task HandleEventAsync_FilterReturnsTrue_PublishesIntegrationMessage(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
||||||
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
||||||
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(true);
|
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(true);
|
||||||
eventMessage.OrganizationId = _organizationId;
|
|
||||||
|
|
||||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
@@ -435,6 +564,7 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_InvalidFilter_LogsErrorDoesNothing(EventMessage eventMessage)
|
public async Task HandleEventAsync_InvalidFilter_LogsErrorDoesNothing(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
var sutProvider = GetSutProvider(InvalidFilterConfiguration());
|
var sutProvider = GetSutProvider(InvalidFilterConfiguration());
|
||||||
|
|
||||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
@@ -444,12 +574,13 @@ public class EventIntegrationHandlerTests
|
|||||||
Arg.Any<EventId>(),
|
Arg.Any<EventId>(),
|
||||||
Arg.Any<object>(),
|
Arg.Any<object>(),
|
||||||
Arg.Any<JsonException>(),
|
Arg.Any<JsonException>(),
|
||||||
Arg.Any<Func<object, Exception, string>>());
|
Arg.Any<Func<object, Exception?, string>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)
|
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)
|
||||||
{
|
{
|
||||||
|
eventMessages.ForEach(e => e.OrganizationId = _organizationId);
|
||||||
var sutProvider = GetSutProvider(NoConfigurations());
|
var sutProvider = GetSutProvider(NoConfigurations());
|
||||||
|
|
||||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||||
@@ -459,13 +590,14 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessages(List<EventMessage> eventMessages)
|
public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessages(List<EventMessage> eventMessages)
|
||||||
{
|
{
|
||||||
|
eventMessages.ForEach(e => e.OrganizationId = _organizationId);
|
||||||
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
||||||
|
|
||||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||||
|
|
||||||
foreach (var eventMessage in eventMessages)
|
foreach (var eventMessage in eventMessages)
|
||||||
{
|
{
|
||||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
|
var expectedMessage = 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(
|
||||||
@@ -477,13 +609,14 @@ public class EventIntegrationHandlerTests
|
|||||||
public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(
|
public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(
|
||||||
List<EventMessage> eventMessages)
|
List<EventMessage> eventMessages)
|
||||||
{
|
{
|
||||||
|
eventMessages.ForEach(e => e.OrganizationId = _organizationId);
|
||||||
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
||||||
|
|
||||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||||
|
|
||||||
foreach (var eventMessage in eventMessages)
|
foreach (var eventMessage in eventMessages)
|
||||||
{
|
{
|
||||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
|
var expectedMessage = 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(
|
||||||
@@ -494,4 +627,84 @@ public class EventIntegrationHandlerTests
|
|||||||
expectedMessage, new[] { "MessageId", "OrganizationId" })));
|
expectedMessage, new[] { "MessageId", "OrganizationId" })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_CapturedFactories_CallConfigurationRepository(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
|
var sutProvider = GetSutProvider(NoConfigurations());
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
var configurationRepository = sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>();
|
||||||
|
|
||||||
|
var configs = OneConfiguration(_templateBase);
|
||||||
|
|
||||||
|
configurationRepository.GetManyByEventTypeOrganizationIdIntegrationType(eventType: eventMessage.Type, organizationId: _organizationId, integrationType: IntegrationType.Webhook).Returns(configs);
|
||||||
|
|
||||||
|
// Capture the factory function - there will be 1 call that returns both specific and wildcard matches
|
||||||
|
Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>? capturedFactory = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(f
|
||||||
|
=> capturedFactory = f),
|
||||||
|
options: Arg.Any<FusionCacheEntryOptions>(),
|
||||||
|
tags: Arg.Any<IEnumerable<string>>()
|
||||||
|
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
|
// Verify factory was captured
|
||||||
|
Assert.NotNull(capturedFactory);
|
||||||
|
|
||||||
|
// Execute the captured factory to trigger repository call
|
||||||
|
await capturedFactory(null!, CancellationToken.None);
|
||||||
|
|
||||||
|
await configurationRepository.Received(1).GetManyByEventTypeOrganizationIdIntegrationType(eventType: eventMessage.Type, organizationId: _organizationId, integrationType: IntegrationType.Webhook);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_ConfigurationCacheOptions_SetsDurationToConstant(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
|
var sutProvider = GetSutProvider(NoConfigurations());
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
|
||||||
|
FusionCacheEntryOptions? capturedOption = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
|
||||||
|
options: Arg.Do<FusionCacheEntryOptions>(opt => capturedOption = opt),
|
||||||
|
tags: Arg.Any<IEnumerable<string>?>()
|
||||||
|
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
|
Assert.NotNull(capturedOption);
|
||||||
|
Assert.Equal(EventIntegrationsCacheConstants.DurationForOrganizationIntegrationConfigurationDetails,
|
||||||
|
capturedOption.Duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_ConfigurationCache_AddsOrganizationIntegrationTag(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
|
var sutProvider = GetSutProvider(NoConfigurations());
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
|
||||||
|
IEnumerable<string>? capturedTags = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
|
||||||
|
options: Arg.Any<FusionCacheEntryOptions>(),
|
||||||
|
tags: Arg.Do<IEnumerable<string>>(t => capturedTags = t)
|
||||||
|
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
|
var expectedTag = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
_organizationId,
|
||||||
|
IntegrationType.Webhook
|
||||||
|
);
|
||||||
|
Assert.NotNull(capturedTags);
|
||||||
|
Assert.Contains(expectedTag, capturedTags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System.Text.Json;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data.Organizations;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Test.Common.AutoFixture;
|
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NSubstitute;
|
|
||||||
using NSubstitute.ExceptionExtensions;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Bit.Core.Test.Services;
|
|
||||||
|
|
||||||
[SutProviderCustomize]
|
|
||||||
public class IntegrationConfigurationDetailsCacheServiceTests
|
|
||||||
{
|
|
||||||
private SutProvider<IntegrationConfigurationDetailsCacheService> GetSutProvider(
|
|
||||||
List<OrganizationIntegrationConfigurationDetails> configurations)
|
|
||||||
{
|
|
||||||
var configurationRepository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
|
|
||||||
configurationRepository.GetAllConfigurationDetailsAsync().Returns(configurations);
|
|
||||||
|
|
||||||
return new SutProvider<IntegrationConfigurationDetailsCacheService>()
|
|
||||||
.SetDependency(configurationRepository)
|
|
||||||
.Create();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetConfigurationDetails_SpecificKeyExists_ReturnsExpectedList(OrganizationIntegrationConfigurationDetails config)
|
|
||||||
{
|
|
||||||
config.EventType = EventType.Cipher_Created;
|
|
||||||
var sutProvider = GetSutProvider([config]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
var result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
config.OrganizationId,
|
|
||||||
config.IntegrationType,
|
|
||||||
EventType.Cipher_Created);
|
|
||||||
Assert.Single(result);
|
|
||||||
Assert.Same(config, result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetConfigurationDetails_AllEventsKeyExists_ReturnsExpectedList(OrganizationIntegrationConfigurationDetails config)
|
|
||||||
{
|
|
||||||
config.EventType = null;
|
|
||||||
var sutProvider = GetSutProvider([config]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
var result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
config.OrganizationId,
|
|
||||||
config.IntegrationType,
|
|
||||||
EventType.Cipher_Created);
|
|
||||||
Assert.Single(result);
|
|
||||||
Assert.Same(config, result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetConfigurationDetails_BothSpecificAndAllEventsKeyExists_ReturnsExpectedList(
|
|
||||||
OrganizationIntegrationConfigurationDetails specificConfig,
|
|
||||||
OrganizationIntegrationConfigurationDetails allKeysConfig
|
|
||||||
)
|
|
||||||
{
|
|
||||||
specificConfig.EventType = EventType.Cipher_Created;
|
|
||||||
allKeysConfig.EventType = null;
|
|
||||||
allKeysConfig.OrganizationId = specificConfig.OrganizationId;
|
|
||||||
allKeysConfig.IntegrationType = specificConfig.IntegrationType;
|
|
||||||
|
|
||||||
var sutProvider = GetSutProvider([specificConfig, allKeysConfig]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
var result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
specificConfig.OrganizationId,
|
|
||||||
specificConfig.IntegrationType,
|
|
||||||
EventType.Cipher_Created);
|
|
||||||
Assert.Equal(2, result.Count);
|
|
||||||
Assert.Contains(result, r => r.Template == specificConfig.Template);
|
|
||||||
Assert.Contains(result, r => r.Template == allKeysConfig.Template);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetConfigurationDetails_KeyMissing_ReturnsEmptyList(OrganizationIntegrationConfigurationDetails config)
|
|
||||||
{
|
|
||||||
var sutProvider = GetSutProvider([config]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
var result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
Guid.NewGuid(),
|
|
||||||
config.IntegrationType,
|
|
||||||
config.EventType ?? EventType.Cipher_Created);
|
|
||||||
Assert.Empty(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetConfigurationDetails_ReturnsCachedValue_EvenIfRepositoryChanges(OrganizationIntegrationConfigurationDetails config)
|
|
||||||
{
|
|
||||||
var sutProvider = GetSutProvider([config]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
|
|
||||||
var newConfig = JsonSerializer.Deserialize<OrganizationIntegrationConfigurationDetails>(JsonSerializer.Serialize(config));
|
|
||||||
Assert.NotNull(newConfig);
|
|
||||||
newConfig.Template = "Changed";
|
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().GetAllConfigurationDetailsAsync()
|
|
||||||
.Returns([newConfig]);
|
|
||||||
|
|
||||||
var result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
config.OrganizationId,
|
|
||||||
config.IntegrationType,
|
|
||||||
config.EventType ?? EventType.Cipher_Created);
|
|
||||||
Assert.Single(result);
|
|
||||||
Assert.NotEqual("Changed", result[0].Template); // should not yet pick up change from repository
|
|
||||||
|
|
||||||
await sutProvider.Sut.RefreshAsync(); // Pick up changes
|
|
||||||
|
|
||||||
result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
config.OrganizationId,
|
|
||||||
config.IntegrationType,
|
|
||||||
config.EventType ?? EventType.Cipher_Created);
|
|
||||||
Assert.Single(result);
|
|
||||||
Assert.Equal("Changed", result[0].Template); // Should have the new value
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RefreshAsync_GroupsByCompositeKey(OrganizationIntegrationConfigurationDetails config1)
|
|
||||||
{
|
|
||||||
var config2 = JsonSerializer.Deserialize<OrganizationIntegrationConfigurationDetails>(
|
|
||||||
JsonSerializer.Serialize(config1))!;
|
|
||||||
config2.Template = "Another";
|
|
||||||
|
|
||||||
var sutProvider = GetSutProvider([config1, config2]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
|
|
||||||
var results = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
config1.OrganizationId,
|
|
||||||
config1.IntegrationType,
|
|
||||||
config1.EventType ?? EventType.Cipher_Created);
|
|
||||||
|
|
||||||
Assert.Equal(2, results.Count);
|
|
||||||
Assert.Contains(results, r => r.Template == config1.Template);
|
|
||||||
Assert.Contains(results, r => r.Template == config2.Template);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RefreshAsync_LogsInformationOnSuccess(OrganizationIntegrationConfigurationDetails config)
|
|
||||||
{
|
|
||||||
var sutProvider = GetSutProvider([config]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ILogger<IntegrationConfigurationDetailsCacheService>>().Received().Log(
|
|
||||||
LogLevel.Information,
|
|
||||||
Arg.Any<EventId>(),
|
|
||||||
Arg.Is<object>(o => o.ToString()!.Contains("Refreshed successfully")),
|
|
||||||
null,
|
|
||||||
Arg.Any<Func<object, Exception?, string>>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RefreshAsync_OnException_LogsError()
|
|
||||||
{
|
|
||||||
var sutProvider = GetSutProvider([]);
|
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().GetAllConfigurationDetailsAsync()
|
|
||||||
.Throws(new Exception("Database failure"));
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ILogger<IntegrationConfigurationDetailsCacheService>>().Received(1).Log(
|
|
||||||
LogLevel.Error,
|
|
||||||
Arg.Any<EventId>(),
|
|
||||||
Arg.Is<object>(o => o.ToString()!.Contains("Refresh failed")),
|
|
||||||
Arg.Any<Exception>(),
|
|
||||||
Arg.Any<Func<object, Exception?, string>>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -11,8 +12,12 @@ public class EventIntegrationsCacheConstantsTests
|
|||||||
{
|
{
|
||||||
var expected = $"Group:{groupId:N}";
|
var expected = $"Group:{groupId:N}";
|
||||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(groupId);
|
var key = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(groupId);
|
||||||
|
var keyWithDifferentGroup = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(Guid.NewGuid());
|
||||||
|
var keyWithSameGroup = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(groupId);
|
||||||
|
|
||||||
Assert.Equal(expected, key);
|
Assert.Equal(expected, key);
|
||||||
|
Assert.NotEqual(key, keyWithDifferentGroup);
|
||||||
|
Assert.Equal(key, keyWithSameGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@@ -20,8 +25,69 @@ public class EventIntegrationsCacheConstantsTests
|
|||||||
{
|
{
|
||||||
var expected = $"Organization:{orgId:N}";
|
var expected = $"Organization:{orgId:N}";
|
||||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(orgId);
|
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(orgId);
|
||||||
|
var keyWithDifferentOrg = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(Guid.NewGuid());
|
||||||
|
var keyWithSameOrg = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(orgId);
|
||||||
|
|
||||||
Assert.Equal(expected, key);
|
Assert.Equal(expected, key);
|
||||||
|
Assert.NotEqual(key, keyWithDifferentOrg);
|
||||||
|
Assert.Equal(key, keyWithSameOrg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void BuildCacheKeyForOrganizationIntegrationConfigurationDetails_ReturnsExpectedKey(Guid orgId)
|
||||||
|
{
|
||||||
|
var integrationType = IntegrationType.Hec;
|
||||||
|
|
||||||
|
var expectedWithEvent = $"OrganizationIntegrationConfigurationDetails:{orgId:N}:Hec:User_LoggedIn";
|
||||||
|
var keyWithEvent = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, integrationType, EventType.User_LoggedIn);
|
||||||
|
var keyWithDifferentEvent = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, integrationType, EventType.Cipher_Created);
|
||||||
|
var keyWithDifferentIntegration = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, IntegrationType.Webhook, EventType.User_LoggedIn);
|
||||||
|
var keyWithDifferentOrganization = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
Guid.NewGuid(), integrationType, EventType.User_LoggedIn);
|
||||||
|
var keyWithSameDetails = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, integrationType, EventType.User_LoggedIn);
|
||||||
|
|
||||||
|
Assert.Equal(expectedWithEvent, keyWithEvent);
|
||||||
|
Assert.NotEqual(keyWithEvent, keyWithDifferentEvent);
|
||||||
|
Assert.NotEqual(keyWithEvent, keyWithDifferentIntegration);
|
||||||
|
Assert.NotEqual(keyWithEvent, keyWithDifferentOrganization);
|
||||||
|
Assert.Equal(keyWithEvent, keyWithSameDetails);
|
||||||
|
|
||||||
|
var expectedWithNullEvent = $"OrganizationIntegrationConfigurationDetails:{orgId:N}:Hec:";
|
||||||
|
var keyWithNullEvent = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, integrationType, null);
|
||||||
|
var keyWithNullEventDifferentIntegration = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, IntegrationType.Webhook, null);
|
||||||
|
var keyWithNullEventDifferentOrganization = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
Guid.NewGuid(), integrationType, null);
|
||||||
|
|
||||||
|
Assert.Equal(expectedWithNullEvent, keyWithNullEvent);
|
||||||
|
Assert.NotEqual(keyWithEvent, keyWithNullEvent);
|
||||||
|
Assert.NotEqual(keyWithNullEvent, keyWithDifferentEvent);
|
||||||
|
Assert.NotEqual(keyWithNullEvent, keyWithNullEventDifferentIntegration);
|
||||||
|
Assert.NotEqual(keyWithNullEvent, keyWithNullEventDifferentOrganization);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void BuildCacheTagForOrganizationIntegration_ReturnsExpectedKey(Guid orgId)
|
||||||
|
{
|
||||||
|
var expected = $"OrganizationIntegration:{orgId:N}:Hec";
|
||||||
|
var tag = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
orgId, IntegrationType.Hec);
|
||||||
|
var tagWithDifferentOrganization = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
Guid.NewGuid(), IntegrationType.Hec);
|
||||||
|
var tagWithDifferentIntegrationType = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
orgId, IntegrationType.Webhook);
|
||||||
|
var tagWithSameDetails = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
orgId, IntegrationType.Hec);
|
||||||
|
|
||||||
|
Assert.Equal(expected, tag);
|
||||||
|
Assert.NotEqual(tag, tagWithDifferentOrganization);
|
||||||
|
Assert.NotEqual(tag, tagWithDifferentIntegrationType);
|
||||||
|
Assert.Equal(tag, tagWithSameDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@@ -29,8 +95,14 @@ public class EventIntegrationsCacheConstantsTests
|
|||||||
{
|
{
|
||||||
var expected = $"OrganizationUserUserDetails:{orgId:N}:{userId:N}";
|
var expected = $"OrganizationUserUserDetails:{orgId:N}:{userId:N}";
|
||||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, userId);
|
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, userId);
|
||||||
|
var keyWithDifferentOrg = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(Guid.NewGuid(), userId);
|
||||||
|
var keyWithDifferentUser = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, Guid.NewGuid());
|
||||||
|
var keyWithSameDetails = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, userId);
|
||||||
|
|
||||||
Assert.Equal(expected, key);
|
Assert.Equal(expected, key);
|
||||||
|
Assert.NotEqual(key, keyWithDifferentOrg);
|
||||||
|
Assert.NotEqual(key, keyWithDifferentUser);
|
||||||
|
Assert.Equal(key, keyWithSameDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -38,4 +110,13 @@ public class EventIntegrationsCacheConstantsTests
|
|||||||
{
|
{
|
||||||
Assert.Equal("EventIntegrations", EventIntegrationsCacheConstants.CacheName);
|
Assert.Equal("EventIntegrations", EventIntegrationsCacheConstants.CacheName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DurationForOrganizationIntegrationConfigurationDetails_ReturnsExpected()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
TimeSpan.FromDays(1),
|
||||||
|
EventIntegrationsCacheConstants.DurationForOrganizationIntegrationConfigurationDetails
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType]
|
||||||
|
@EventType SMALLINT,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@IntegrationType SMALLINT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
oic.*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
|
||||||
|
WHERE
|
||||||
|
(oic.[EventType] = @EventType OR oic.[EventType] IS NULL)
|
||||||
|
AND
|
||||||
|
oic.[OrganizationId] = @OrganizationId
|
||||||
|
AND
|
||||||
|
oic.[IntegrationType] = @IntegrationType
|
||||||
|
END
|
||||||
|
GO
|
||||||
Reference in New Issue
Block a user