1
0
mirror of https://github.com/bitwarden/server synced 2025-12-20 02:03:46 +00:00

Add CQRS and caching support for OrganizationIntegrationConfigurations (#6690)

This commit is contained in:
Brant DeBow
2025-12-12 11:52:32 -05:00
committed by GitHub
parent 3de2f98681
commit 72c8967937
23 changed files with 1676 additions and 1105 deletions

View File

@@ -1,8 +1,8 @@
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -12,8 +12,10 @@ namespace Bit.Api.AdminConsole.Controllers;
[Authorize("Application")] [Authorize("Application")]
public class OrganizationIntegrationConfigurationController( public class OrganizationIntegrationConfigurationController(
ICurrentContext currentContext, ICurrentContext currentContext,
IOrganizationIntegrationRepository integrationRepository, ICreateOrganizationIntegrationConfigurationCommand createCommand,
IOrganizationIntegrationConfigurationRepository integrationConfigurationRepository) : Controller IUpdateOrganizationIntegrationConfigurationCommand updateCommand,
IDeleteOrganizationIntegrationConfigurationCommand deleteCommand,
IGetOrganizationIntegrationConfigurationsQuery getQuery) : Controller
{ {
[HttpGet("")] [HttpGet("")]
public async Task<List<OrganizationIntegrationConfigurationResponseModel>> GetAsync( public async Task<List<OrganizationIntegrationConfigurationResponseModel>> GetAsync(
@@ -24,13 +26,8 @@ public class OrganizationIntegrationConfigurationController(
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
var configurations = await integrationConfigurationRepository.GetManyByIntegrationAsync(integrationId); var configurations = await getQuery.GetManyByIntegrationAsync(organizationId, integrationId);
return configurations return configurations
.Select(configuration => new OrganizationIntegrationConfigurationResponseModel(configuration)) .Select(configuration => new OrganizationIntegrationConfigurationResponseModel(configuration))
.ToList(); .ToList();
@@ -46,19 +43,11 @@ public class OrganizationIntegrationConfigurationController(
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
if (!model.IsValidForType(integration.Type))
{
throw new BadRequestException($"Invalid Configuration and/or Template for integration type {integration.Type}");
}
var organizationIntegrationConfiguration = model.ToOrganizationIntegrationConfiguration(integrationId); var configuration = model.ToOrganizationIntegrationConfiguration(integrationId);
var configuration = await integrationConfigurationRepository.CreateAsync(organizationIntegrationConfiguration); var created = await createCommand.CreateAsync(organizationId, integrationId, configuration);
return new OrganizationIntegrationConfigurationResponseModel(configuration);
return new OrganizationIntegrationConfigurationResponseModel(created);
} }
[HttpPut("{configurationId:guid}")] [HttpPut("{configurationId:guid}")]
@@ -72,26 +61,11 @@ public class OrganizationIntegrationConfigurationController(
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
if (!model.IsValidForType(integration.Type))
{
throw new BadRequestException($"Invalid Configuration and/or Template for integration type {integration.Type}");
}
var configuration = await integrationConfigurationRepository.GetByIdAsync(configurationId); var configuration = model.ToOrganizationIntegrationConfiguration(integrationId);
if (configuration is null || configuration.OrganizationIntegrationId != integrationId) var updated = await updateCommand.UpdateAsync(organizationId, integrationId, configurationId, configuration);
{
throw new NotFoundException();
}
var newConfiguration = model.ToOrganizationIntegrationConfiguration(configuration); return new OrganizationIntegrationConfigurationResponseModel(updated);
await integrationConfigurationRepository.ReplaceAsync(newConfiguration);
return new OrganizationIntegrationConfigurationResponseModel(newConfiguration);
} }
[HttpDelete("{configurationId:guid}")] [HttpDelete("{configurationId:guid}")]
@@ -101,19 +75,8 @@ public class OrganizationIntegrationConfigurationController(
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
var configuration = await integrationConfigurationRepository.GetByIdAsync(configurationId); await deleteCommand.DeleteAsync(organizationId, integrationId, configurationId);
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
{
throw new NotFoundException();
}
await integrationConfigurationRepository.DeleteAsync(configuration);
} }
[HttpPost("{configurationId:guid}/delete")] [HttpPost("{configurationId:guid}/delete")]

View File

@@ -1,6 +1,4 @@
using System.Text.Json; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums; using Bit.Core.Enums;
@@ -16,38 +14,6 @@ public class OrganizationIntegrationConfigurationRequestModel
public string? Template { get; set; } public string? Template { get; set; }
public bool IsValidForType(IntegrationType integrationType)
{
switch (integrationType)
{
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
return false;
case IntegrationType.Slack:
return !string.IsNullOrWhiteSpace(Template) &&
IsConfigurationValid<SlackIntegrationConfiguration>() &&
IsFiltersValid();
case IntegrationType.Webhook:
return !string.IsNullOrWhiteSpace(Template) &&
IsConfigurationValid<WebhookIntegrationConfiguration>() &&
IsFiltersValid();
case IntegrationType.Hec:
return !string.IsNullOrWhiteSpace(Template) &&
Configuration is null &&
IsFiltersValid();
case IntegrationType.Datadog:
return !string.IsNullOrWhiteSpace(Template) &&
Configuration is null &&
IsFiltersValid();
case IntegrationType.Teams:
return !string.IsNullOrWhiteSpace(Template) &&
Configuration is null &&
IsFiltersValid();
default:
return false;
}
}
public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(Guid organizationIntegrationId) public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(Guid organizationIntegrationId)
{ {
return new OrganizationIntegrationConfiguration() return new OrganizationIntegrationConfiguration()
@@ -59,50 +25,4 @@ public class OrganizationIntegrationConfigurationRequestModel
Template = Template Template = Template
}; };
} }
public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(OrganizationIntegrationConfiguration currentConfiguration)
{
currentConfiguration.Configuration = Configuration;
currentConfiguration.EventType = EventType;
currentConfiguration.Filters = Filters;
currentConfiguration.Template = Template;
return currentConfiguration;
}
private bool IsConfigurationValid<T>()
{
if (string.IsNullOrWhiteSpace(Configuration))
{
return false;
}
try
{
var config = JsonSerializer.Deserialize<T>(Configuration);
return config is not null;
}
catch
{
return false;
}
}
private bool IsFiltersValid()
{
if (Filters is null)
{
return true;
}
try
{
var filters = JsonSerializer.Deserialize<IntegrationFilterGroup>(Filters);
return filters is not null;
}
catch
{
return false;
}
}
} }

View File

@@ -1,5 +1,8 @@
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -20,8 +23,12 @@ public static class EventIntegrationsServiceCollectionExtensions
// This is idempotent for the same named cache, so it's safe to call. // This is idempotent for the same named cache, so it's safe to call.
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings); services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
// Add Validator
services.TryAddSingleton<IOrganizationIntegrationConfigurationValidator, OrganizationIntegrationConfigurationValidator>();
// Add all commands/queries // Add all commands/queries
services.AddOrganizationIntegrationCommandsQueries(); services.AddOrganizationIntegrationCommandsQueries();
services.AddOrganizationIntegrationConfigurationCommandsQueries();
return services; return services;
} }
@@ -35,4 +42,14 @@ public static class EventIntegrationsServiceCollectionExtensions
return services; return services;
} }
internal static IServiceCollection AddOrganizationIntegrationConfigurationCommandsQueries(this IServiceCollection services)
{
services.TryAddScoped<ICreateOrganizationIntegrationConfigurationCommand, CreateOrganizationIntegrationConfigurationCommand>();
services.TryAddScoped<IUpdateOrganizationIntegrationConfigurationCommand, UpdateOrganizationIntegrationConfigurationCommand>();
services.TryAddScoped<IDeleteOrganizationIntegrationConfigurationCommand, DeleteOrganizationIntegrationConfigurationCommand>();
services.TryAddScoped<IGetOrganizationIntegrationConfigurationsQuery, GetOrganizationIntegrationConfigurationsQuery>();
return services;
}
} }

View File

@@ -0,0 +1,64 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.Extensions.DependencyInjection;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
/// <summary>
/// Command implementation for creating organization integration configurations with validation and cache invalidation support.
/// </summary>
public class CreateOrganizationIntegrationConfigurationCommand(
IOrganizationIntegrationRepository integrationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository,
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache,
IOrganizationIntegrationConfigurationValidator validator)
: ICreateOrganizationIntegrationConfigurationCommand
{
public async Task<OrganizationIntegrationConfiguration> CreateAsync(
Guid organizationId,
Guid integrationId,
OrganizationIntegrationConfiguration configuration)
{
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
if (!validator.ValidateConfiguration(integration.Type, configuration))
{
throw new BadRequestException(
$"Invalid Configuration and/or Filters for integration type {integration.Type}");
}
var created = await configurationRepository.CreateAsync(configuration);
// Invalidate the cached configuration details
// Even though this is a new record, the cache could hold a stale empty list for this
if (created.EventType == null)
{
// Wildcard configuration - invalidate all cached results for this org/integration
await cache.RemoveByTagAsync(
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId: organizationId,
integrationType: integration.Type
));
}
else
{
// Specific event type - only invalidate that specific cache entry
await cache.RemoveAsync(
EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId: organizationId,
integrationType: integration.Type,
eventType: created.EventType.Value
));
}
return created;
}
}

View File

@@ -0,0 +1,54 @@
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.Extensions.DependencyInjection;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
/// <summary>
/// Command implementation for deleting organization integration configurations with cache invalidation support.
/// </summary>
public class DeleteOrganizationIntegrationConfigurationCommand(
IOrganizationIntegrationRepository integrationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository,
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache)
: IDeleteOrganizationIntegrationConfigurationCommand
{
public async Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)
{
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
var configuration = await configurationRepository.GetByIdAsync(configurationId);
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
{
throw new NotFoundException();
}
await configurationRepository.DeleteAsync(configuration);
if (configuration.EventType == null)
{
// Wildcard configuration - invalidate all cached results for this org/integration
await cache.RemoveByTagAsync(
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId: organizationId,
integrationType: integration.Type
));
}
else
{
// Specific event type - only invalidate that specific cache entry
await cache.RemoveAsync(
EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId: organizationId,
integrationType: integration.Type,
eventType: configuration.EventType.Value
));
}
}
}

View File

@@ -0,0 +1,29 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
/// <summary>
/// Query implementation for retrieving organization integration configurations.
/// </summary>
public class GetOrganizationIntegrationConfigurationsQuery(
IOrganizationIntegrationRepository integrationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository)
: IGetOrganizationIntegrationConfigurationsQuery
{
public async Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(
Guid organizationId,
Guid integrationId)
{
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
var configurations = await configurationRepository.GetManyByIntegrationAsync(integrationId);
return configurations.ToList();
}
}

View File

@@ -0,0 +1,22 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
/// <summary>
/// Command interface for creating organization integration configurations.
/// </summary>
public interface ICreateOrganizationIntegrationConfigurationCommand
{
/// <summary>
/// Creates a new configuration for an organization integration.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization.</param>
/// <param name="integrationId">The unique identifier of the integration.</param>
/// <param name="configuration">The configuration to create.</param>
/// <returns>The created configuration.</returns>
/// <exception cref="Exceptions.NotFoundException">Thrown when the integration does not exist
/// or does not belong to the specified organization.</exception>
/// <exception cref="Exceptions.BadRequestException">Thrown when the configuration or filters
/// are invalid for the integration type.</exception>
Task<OrganizationIntegrationConfiguration> CreateAsync(Guid organizationId, Guid integrationId, OrganizationIntegrationConfiguration configuration);
}

View File

@@ -0,0 +1,19 @@
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
/// <summary>
/// Command interface for deleting organization integration configurations.
/// </summary>
public interface IDeleteOrganizationIntegrationConfigurationCommand
{
/// <summary>
/// Deletes a configuration from an organization integration.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization.</param>
/// <param name="integrationId">The unique identifier of the integration.</param>
/// <param name="configurationId">The unique identifier of the configuration to delete.</param>
/// <exception cref="Exceptions.NotFoundException">
/// Thrown when the integration or configuration does not exist,
/// or the integration does not belong to the specified organization,
/// or the configuration does not belong to the specified integration.</exception>
Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId);
}

View File

@@ -0,0 +1,19 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
/// <summary>
/// Query interface for retrieving organization integration configurations.
/// </summary>
public interface IGetOrganizationIntegrationConfigurationsQuery
{
/// <summary>
/// Retrieves all configurations for a specific organization integration.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization.</param>
/// <param name="integrationId">The unique identifier of the integration.</param>
/// <returns>A list of configurations associated with the integration.</returns>
/// <exception cref="Exceptions.NotFoundException">Thrown when the integration does not exist
/// or does not belong to the specified organization.</exception>
Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(Guid organizationId, Guid integrationId);
}

View File

@@ -0,0 +1,25 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
/// <summary>
/// Command interface for updating organization integration configurations.
/// </summary>
public interface IUpdateOrganizationIntegrationConfigurationCommand
{
/// <summary>
/// Updates an existing configuration for an organization integration.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization.</param>
/// <param name="integrationId">The unique identifier of the integration.</param>
/// <param name="configurationId">The unique identifier of the configuration to update.</param>
/// <param name="updatedConfiguration">The updated configuration data.</param>
/// <returns>The updated configuration.</returns>
/// <exception cref="Exceptions.NotFoundException">
/// Thrown when the integration or the configuration does not exist,
/// or the integration does not belong to the specified organization,
/// or the configuration does not belong to the specified integration.</exception>
/// <exception cref="Exceptions.BadRequestException">Thrown when the configuration or filters
/// are invalid for the integration type.</exception>
Task<OrganizationIntegrationConfiguration> UpdateAsync(Guid organizationId, Guid integrationId, Guid configurationId, OrganizationIntegrationConfiguration updatedConfiguration);
}

View File

@@ -0,0 +1,82 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.Extensions.DependencyInjection;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
/// <summary>
/// Command implementation for updating organization integration configurations with validation and cache invalidation support.
/// </summary>
public class UpdateOrganizationIntegrationConfigurationCommand(
IOrganizationIntegrationRepository integrationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository,
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache,
IOrganizationIntegrationConfigurationValidator validator)
: IUpdateOrganizationIntegrationConfigurationCommand
{
public async Task<OrganizationIntegrationConfiguration> UpdateAsync(
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegrationConfiguration updatedConfiguration)
{
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
var configuration = await configurationRepository.GetByIdAsync(configurationId);
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
{
throw new NotFoundException();
}
if (!validator.ValidateConfiguration(integration.Type, updatedConfiguration))
{
throw new BadRequestException($"Invalid Configuration and/or Filters for integration type {integration.Type}");
}
updatedConfiguration.Id = configuration.Id;
updatedConfiguration.CreationDate = configuration.CreationDate;
await configurationRepository.ReplaceAsync(updatedConfiguration);
// If either old or new EventType is null (wildcard), invalidate all cached results
// for the specific integration
if (configuration.EventType == null || updatedConfiguration.EventType == null)
{
// Wildcard involved - invalidate all cached results for this org/integration
await cache.RemoveByTagAsync(
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId: organizationId,
integrationType: integration.Type
));
return updatedConfiguration;
}
// Both are specific event types - invalidate specific cache entries
await cache.RemoveAsync(
EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId: organizationId,
integrationType: integration.Type,
eventType: configuration.EventType.Value
));
// If event type changed, also clear the new event type's cache
if (configuration.EventType != updatedConfiguration.EventType)
{
await cache.RemoveAsync(
EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId: organizationId,
integrationType: integration.Type,
eventType: updatedConfiguration.EventType.Value
));
}
return updatedConfiguration;
}
}

View File

@@ -0,0 +1,17 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.Services;
public interface IOrganizationIntegrationConfigurationValidator
{
/// <summary>
/// Validates that the configuration is valid for the given integration type. The configuration must
/// include a Configuration that is valid for the type, valid Filters, and a non-empty Template
/// to pass validation.
/// </summary>
/// <param name="integrationType">The type of integration</param>
/// <param name="configuration">The OrganizationIntegrationConfiguration to validate</param>
/// <returns>True if valid, false otherwise</returns>
bool ValidateConfiguration(IntegrationType integrationType, OrganizationIntegrationConfiguration configuration);
}

View File

@@ -0,0 +1,76 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.Services;
public class OrganizationIntegrationConfigurationValidator : IOrganizationIntegrationConfigurationValidator
{
public bool ValidateConfiguration(IntegrationType integrationType,
OrganizationIntegrationConfiguration configuration)
{
// Validate template is present
if (string.IsNullOrWhiteSpace(configuration.Template))
{
return false;
}
// If Filters are present, they must be valid
if (!IsFiltersValid(configuration.Filters))
{
return false;
}
switch (integrationType)
{
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
return false;
case IntegrationType.Slack:
return IsConfigurationValid<SlackIntegrationConfiguration>(configuration.Configuration);
case IntegrationType.Webhook:
return IsConfigurationValid<WebhookIntegrationConfiguration>(configuration.Configuration);
case IntegrationType.Hec:
case IntegrationType.Datadog:
case IntegrationType.Teams:
return configuration.Configuration is null;
default:
return false;
}
}
private static bool IsConfigurationValid<T>(string? configuration)
{
if (string.IsNullOrWhiteSpace(configuration))
{
return false;
}
try
{
var config = JsonSerializer.Deserialize<T>(configuration);
return config is not null;
}
catch
{
return false;
}
}
private static bool IsFiltersValid(string? filters)
{
if (filters is null)
{
return true;
}
try
{
var filterGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(filters);
return filterGroup is not null;
}
catch
{
return false;
}
}
}

View File

@@ -55,16 +55,16 @@ public static class EventIntegrationsCacheConstants
/// Builds a deterministic cache key for an organization's integration configuration details /// Builds a deterministic cache key for an organization's integration configuration details
/// <see cref="OrganizationIntegrationConfigurationDetails"/>. /// <see cref="OrganizationIntegrationConfigurationDetails"/>.
/// </summary> /// </summary>
/// <param name="organizationId">The unique identifier of the organization to which the user belongs.</param> /// <param name="organizationId">The unique identifier of the organization.</param>
/// <param name="integrationType">The <see cref="IntegrationType"/> of the integration.</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> /// <param name="eventType">The specific <see cref="EventType"/> of the event configured.</param>
/// <returns> /// <returns>
/// A cache key for the configuration details. /// A cache key for the configuration details.
/// </returns> /// </returns>
public static string BuildCacheKeyForOrganizationIntegrationConfigurationDetails( public static string BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
Guid organizationId, Guid organizationId,
IntegrationType integrationType, IntegrationType integrationType,
EventType? eventType EventType eventType
) => $"OrganizationIntegrationConfigurationDetails:{organizationId:N}:{integrationType}:{eventType}"; ) => $"OrganizationIntegrationConfigurationDetails:{organizationId:N}:{integrationType}:{eventType}";
/// <summary> /// <summary>

View File

@@ -1,18 +1,14 @@
using System.Text.Json; using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NSubstitute; using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit; using Xunit;
namespace Bit.Api.Test.AdminConsole.Controllers; namespace Bit.Api.Test.AdminConsole.Controllers;
@@ -25,823 +21,191 @@ public class OrganizationIntegrationsConfigurationControllerTests
public async Task DeleteAsync_AllParamsProvided_Succeeds( public async Task DeleteAsync_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider, SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId, Guid organizationId,
OrganizationIntegration organizationIntegration, Guid integrationId,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration) Guid configurationId)
{ {
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
await sutProvider.Sut.DeleteAsync(organizationId, organizationIntegration.Id, organizationIntegrationConfiguration.Id); await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1) await sutProvider.GetDependency<IDeleteOrganizationIntegrationConfigurationCommand>().Received(1)
.GetByIdAsync(organizationIntegration.Id); .DeleteAsync(organizationId, integrationId, configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(organizationIntegrationConfiguration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.DeleteAsync(organizationIntegrationConfiguration);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
[Obsolete("Obsolete")]
public async Task PostDeleteAsync_AllParamsProvided_Succeeds( public async Task PostDeleteAsync_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider, SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId, Guid organizationId,
OrganizationIntegration organizationIntegration, Guid integrationId,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration) Guid configurationId)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
await sutProvider.Sut.PostDeleteAsync(organizationId, organizationIntegration.Id, organizationIntegrationConfiguration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(organizationIntegrationConfiguration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.DeleteAsync(organizationIntegrationConfiguration);
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
organizationIntegration.OrganizationId = organizationId;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty));
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
{ {
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty)); await sutProvider.Sut.PostDeleteAsync(organizationId, integrationId, configurationId);
}
[Theory, BitAutoData] await sutProvider.GetDependency<IDeleteOrganizationIntegrationConfigurationCommand>().Received(1)
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound( .DeleteAsync(organizationId, integrationId, configurationId);
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, organizationIntegration.Id, Guid.Empty));
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationConfigDoesNotBelongToIntegration_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = Guid.Empty;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, organizationIntegration.Id, Guid.Empty));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task DeleteAsync_UserIsNotOrganizationAdmin_ThrowsNotFound( public async Task DeleteAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider, SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId) Guid organizationId,
Guid integrationId,
Guid configurationId)
{ {
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(false); .Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty)); await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetAsync_ConfigurationsExist_Succeeds( public async Task GetAsync_ConfigurationsExist_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider, SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId, Guid organizationId,
OrganizationIntegration organizationIntegration, Guid integrationId,
List<OrganizationIntegrationConfiguration> organizationIntegrationConfigurations) List<OrganizationIntegrationConfiguration> configurations)
{ {
organizationIntegration.OrganizationId = organizationId;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>() sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>()
.GetByIdAsync(Arg.Any<Guid>()) .GetManyByIntegrationAsync(organizationId, integrationId)
.Returns(organizationIntegration); .Returns(configurations);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(Arg.Any<Guid>()) var result = await sutProvider.Sut.GetAsync(organizationId, integrationId);
.Returns(organizationIntegrationConfigurations);
var result = await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal(organizationIntegrationConfigurations.Count, result.Count); Assert.Equal(configurations.Count, result.Count);
Assert.All(result, r => Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(r)); Assert.All(result, r => Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(r));
await sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>().Received(1)
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1) .GetManyByIntegrationAsync(organizationId, integrationId);
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(organizationIntegration.Id);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetAsync_NoConfigurationsExist_ReturnsEmptyList( public async Task GetAsync_NoConfigurationsExist_ReturnsEmptyList(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider, SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId, Guid organizationId,
OrganizationIntegration organizationIntegration) Guid integrationId)
{ {
organizationIntegration.OrganizationId = organizationId;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>() sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>()
.GetByIdAsync(Arg.Any<Guid>()) .GetManyByIntegrationAsync(organizationId, integrationId)
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(Arg.Any<Guid>())
.Returns([]); .Returns([]);
var result = await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id); var result = await sutProvider.Sut.GetAsync(organizationId, integrationId);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Empty(result); Assert.Empty(result);
await sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>().Received(1)
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1) .GetManyByIntegrationAsync(organizationId, integrationId);
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(organizationIntegration.Id);
}
[Theory, BitAutoData]
public async Task GetAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.NewGuid()));
}
[Theory, BitAutoData]
public async Task GetAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound( public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider, SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId) Guid organizationId,
Guid integrationId)
{ {
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(false); .Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.NewGuid())); await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.GetAsync(organizationId, integrationId));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PostAsync_AllParamsProvided_Slack_Succeeds( public async Task PostAsync_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider, SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId, Guid organizationId,
OrganizationIntegration organizationIntegration, Guid integrationId,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration, OrganizationIntegrationConfiguration configuration,
OrganizationIntegrationConfigurationRequestModel model) OrganizationIntegrationConfigurationRequestModel model)
{ {
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Slack;
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
model.Configuration = JsonSerializer.Serialize(slackConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>() sutProvider.GetDependency<ICreateOrganizationIntegrationConfigurationCommand>()
.GetByIdAsync(Arg.Any<Guid>()) .CreateAsync(organizationId, integrationId, Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegration); .Returns(configuration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1) var createResponse = await sutProvider.Sut.CreateAsync(organizationId, integrationId, model);
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<ICreateOrganizationIntegrationConfigurationCommand>().Received(1)
.CreateAsync(organizationId, integrationId, Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse); Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse);
Assert.Equal(expected.Id, createResponse.Id);
Assert.Equal(expected.Configuration, createResponse.Configuration);
Assert.Equal(expected.EventType, createResponse.EventType);
Assert.Equal(expected.Filters, createResponse.Filters);
Assert.Equal(expected.Template, createResponse.Template);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PostAsync_AllParamsProvided_Webhook_Succeeds( public async Task PostAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider, SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId, Guid organizationId,
OrganizationIntegration organizationIntegration, Guid integrationId)
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse);
Assert.Equal(expected.Id, createResponse.Id);
Assert.Equal(expected.Configuration, createResponse.Configuration);
Assert.Equal(expected.EventType, createResponse.EventType);
Assert.Equal(expected.Filters, createResponse.Filters);
Assert.Equal(expected.Template, createResponse.Template);
}
[Theory, BitAutoData]
public async Task PostAsync_OnlyUrlProvided_Webhook_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"));
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse);
Assert.Equal(expected.Id, createResponse.Id);
Assert.Equal(expected.Configuration, createResponse.Configuration);
Assert.Equal(expected.EventType, createResponse.EventType);
Assert.Equal(expected.Filters, createResponse.Filters);
Assert.Equal(expected.Template, createResponse.Template);
}
[Theory, BitAutoData]
public async Task PostAsync_IntegrationTypeCloudBillingSync_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.CloudBillingSync;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
organizationIntegration.Id,
model));
}
[Theory, BitAutoData]
public async Task PostAsync_IntegrationTypeScim_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Scim;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
organizationIntegration.Id,
model));
}
[Theory, BitAutoData]
public async Task PostAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
Guid.Empty,
new OrganizationIntegrationConfigurationRequestModel()));
}
[Theory, BitAutoData]
public async Task PostAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
organizationIntegration.Id,
new OrganizationIntegrationConfigurationRequestModel()));
}
[Theory, BitAutoData]
public async Task PostAsync_InvalidConfiguration_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
model.Configuration = null;
model.Template = "Template String";
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
organizationIntegration.Id,
model));
}
[Theory, BitAutoData]
public async Task PostAsync_InvalidTemplate_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = null;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
organizationIntegration.Id,
model));
}
[Theory, BitAutoData]
public async Task PostAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<OrganizationIntegrationConfigurationController> sutProvider, Guid organizationId)
{ {
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(false); .Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(organizationId, Guid.Empty, new OrganizationIntegrationConfigurationRequestModel())); await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.CreateAsync(organizationId, integrationId, new OrganizationIntegrationConfigurationRequestModel()));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task UpdateAsync_AllParamsProvided_Slack_Succeeds( public async Task UpdateAsync_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider, SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId, Guid organizationId,
OrganizationIntegration organizationIntegration, Guid integrationId,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration, Guid configurationId,
OrganizationIntegrationConfiguration configuration,
OrganizationIntegrationConfigurationRequestModel model) OrganizationIntegrationConfigurationRequestModel model)
{ {
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Slack;
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
model.Configuration = JsonSerializer.Serialize(slackConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(true); .Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>() sutProvider.GetDependency<IUpdateOrganizationIntegrationConfigurationCommand>()
.GetByIdAsync(Arg.Any<Guid>()) .UpdateAsync(organizationId, integrationId, configurationId, Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegration); .Returns(configuration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1) var updateResponse = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, model);
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IUpdateOrganizationIntegrationConfigurationCommand>().Received(1)
.UpdateAsync(organizationId, integrationId, configurationId, Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse); Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse);
Assert.Equal(expected.Id, updateResponse.Id);
Assert.Equal(expected.Configuration, updateResponse.Configuration);
Assert.Equal(expected.EventType, updateResponse.EventType);
Assert.Equal(expected.Filters, updateResponse.Filters);
Assert.Equal(expected.Template, updateResponse.Template);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task UpdateAsync_AllParamsProvided_Webhook_Succeeds( public async Task UpdateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider, SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId, Guid organizationId,
OrganizationIntegration organizationIntegration, Guid integrationId,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration, Guid configurationId)
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse);
Assert.Equal(expected.Id, updateResponse.Id);
Assert.Equal(expected.Configuration, updateResponse.Configuration);
Assert.Equal(expected.EventType, updateResponse.EventType);
Assert.Equal(expected.Filters, updateResponse.Filters);
Assert.Equal(expected.Template, updateResponse.Template);
}
[Theory, BitAutoData]
public async Task UpdateAsync_OnlyUrlProvided_Webhook_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"));
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse);
Assert.Equal(expected.Id, updateResponse.Id);
Assert.Equal(expected.Configuration, updateResponse.Configuration);
Assert.Equal(expected.EventType, updateResponse.EventType);
Assert.Equal(expected.Filters, updateResponse.Filters);
Assert.Equal(expected.Template, updateResponse.Template);
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
Guid.Empty,
model));
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(
organizationId,
Guid.Empty,
Guid.Empty,
new OrganizationIntegrationConfigurationRequestModel()));
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
Guid.Empty,
new OrganizationIntegrationConfigurationRequestModel()));
}
[Theory, BitAutoData]
public async Task UpdateAsync_InvalidConfiguration_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Slack;
model.Configuration = null;
model.Template = "Template String";
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model));
}
[Theory, BitAutoData]
public async Task UpdateAsync_InvalidTemplate_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Slack;
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
model.Configuration = JsonSerializer.Serialize(slackConfig);
model.Template = null;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model));
}
[Theory, BitAutoData]
public async Task UpdateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<OrganizationIntegrationConfigurationController> sutProvider, Guid organizationId)
{ {
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(false); .Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync( await Assert.ThrowsAsync<NotFoundException>(async () =>
organizationId, await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, new OrganizationIntegrationConfigurationRequestModel()));
Guid.Empty,
Guid.Empty,
new OrganizationIntegrationConfigurationRequestModel()));
} }
} }

View File

@@ -1,248 +0,0 @@
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;
public class OrganizationIntegrationConfigurationRequestModelTests
{
[Fact]
public void IsValidForType_CloudBillingSyncIntegration_ReturnsFalse()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = "{}",
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.CloudBillingSync));
}
[Theory]
[InlineData(data: null)]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyConfiguration_ReturnsFalse(string? config)
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Theory]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyNonNullConfiguration_ReturnsFalse(string? config)
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
Assert.False(condition: model.IsValidForType(IntegrationType.Datadog));
Assert.False(condition: model.IsValidForType(IntegrationType.Teams));
}
[Fact]
public void IsValidForType_NullConfiguration_ReturnsTrue()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = null,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Hec));
Assert.True(condition: model.IsValidForType(IntegrationType.Datadog));
Assert.True(condition: model.IsValidForType(IntegrationType.Teams));
}
[Theory]
[InlineData(data: null)]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyTemplate_ReturnsFalse(string? template)
{
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = template
};
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
Assert.False(condition: model.IsValidForType(IntegrationType.Datadog));
Assert.False(condition: model.IsValidForType(IntegrationType.Teams));
}
[Fact]
public void IsValidForType_InvalidJsonConfiguration_ReturnsFalse()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = "{not valid json}",
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
Assert.False(condition: model.IsValidForType(IntegrationType.Datadog));
Assert.False(condition: model.IsValidForType(IntegrationType.Teams));
}
[Fact]
public void IsValidForType_InvalidJsonFilters_ReturnsFalse()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://example.com")));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Filters = "{Not valid json",
Template = "template"
};
Assert.False(model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_ScimIntegration_ReturnsFalse()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = "{}",
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.Scim));
}
[Fact]
public void IsValidForType_ValidSlackConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(value: new SlackIntegrationConfiguration(ChannelId: "C12345"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Slack));
}
[Fact]
public void IsValidForType_ValidSlackConfigurationWithFilters_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345"));
var filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
Rules = [
new IntegrationFilterRule()
{
Operation = IntegrationFilterOperation.Equals,
Property = "CollectionId",
Value = Guid.NewGuid()
}
],
Groups = []
});
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Filters = filters,
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Slack));
}
[Fact]
public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost")));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_ValidWebhookConfigurationWithFilters_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(
Uri: new Uri("https://example.com"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
Rules = [
new IntegrationFilterRule()
{
Operation = IntegrationFilterOperation.Equals,
Property = "CollectionId",
Value = Guid.NewGuid()
}
],
Groups = []
});
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Filters = filters,
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_UnknownIntegrationType_ReturnsFalse()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = "{}",
Template = "template"
};
var unknownType = (IntegrationType)999;
Assert.False(condition: model.IsValidForType(unknownType));
}
}

View File

@@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@@ -32,6 +34,7 @@ public class EventIntegrationServiceCollectionExtensionsTests
// Mock required repository dependencies for commands // Mock required repository dependencies for commands
_services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationRepository>()); _services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationRepository>());
_services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationConfigurationRepository>());
_services.TryAddScoped(_ => Substitute.For<IOrganizationRepository>()); _services.TryAddScoped(_ => Substitute.For<IOrganizationRepository>());
} }
@@ -45,6 +48,9 @@ public class EventIntegrationServiceCollectionExtensionsTests
var cache = provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName); var cache = provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName);
Assert.NotNull(cache); Assert.NotNull(cache);
var validator = provider.GetRequiredService<IOrganizationIntegrationConfigurationValidator>();
Assert.NotNull(validator);
using var scope = provider.CreateScope(); using var scope = provider.CreateScope();
var sp = scope.ServiceProvider; var sp = scope.ServiceProvider;
@@ -52,6 +58,11 @@ public class EventIntegrationServiceCollectionExtensionsTests
Assert.NotNull(sp.GetService<IUpdateOrganizationIntegrationCommand>()); Assert.NotNull(sp.GetService<IUpdateOrganizationIntegrationCommand>());
Assert.NotNull(sp.GetService<IDeleteOrganizationIntegrationCommand>()); Assert.NotNull(sp.GetService<IDeleteOrganizationIntegrationCommand>());
Assert.NotNull(sp.GetService<IGetOrganizationIntegrationsQuery>()); Assert.NotNull(sp.GetService<IGetOrganizationIntegrationsQuery>());
Assert.NotNull(sp.GetService<ICreateOrganizationIntegrationConfigurationCommand>());
Assert.NotNull(sp.GetService<IUpdateOrganizationIntegrationConfigurationCommand>());
Assert.NotNull(sp.GetService<IDeleteOrganizationIntegrationConfigurationCommand>());
Assert.NotNull(sp.GetService<IGetOrganizationIntegrationConfigurationsQuery>());
} }
[Fact] [Fact]
@@ -61,8 +72,11 @@ public class EventIntegrationServiceCollectionExtensionsTests
var createIntegrationDescriptor = _services.First(s => var createIntegrationDescriptor = _services.First(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationCommand)); s.ServiceType == typeof(ICreateOrganizationIntegrationCommand));
var createConfigDescriptor = _services.First(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand));
Assert.Equal(ServiceLifetime.Scoped, createIntegrationDescriptor.Lifetime); Assert.Equal(ServiceLifetime.Scoped, createIntegrationDescriptor.Lifetime);
Assert.Equal(ServiceLifetime.Scoped, createConfigDescriptor.Lifetime);
} }
[Fact] [Fact]
@@ -117,7 +131,7 @@ public class EventIntegrationServiceCollectionExtensionsTests
_services.AddEventIntegrationsCommandsQueries(_globalSettings); _services.AddEventIntegrationsCommandsQueries(_globalSettings);
var createConfigCmdDescriptors = _services.Where(s => var createConfigCmdDescriptors = _services.Where(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationCommand)).ToList(); s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand)).ToList();
Assert.Single(createConfigCmdDescriptors); Assert.Single(createConfigCmdDescriptors);
var updateIntegrationCmdDescriptors = _services.Where(s => var updateIntegrationCmdDescriptors = _services.Where(s =>
@@ -148,6 +162,29 @@ public class EventIntegrationServiceCollectionExtensionsTests
Assert.Single(createCmdDescriptors); Assert.Single(createCmdDescriptors);
} }
[Fact]
public void AddOrganizationIntegrationConfigurationCommandsQueries_RegistersAllConfigurationServices()
{
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
Assert.Contains(_services, s => s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IUpdateOrganizationIntegrationConfigurationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IDeleteOrganizationIntegrationConfigurationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IGetOrganizationIntegrationConfigurationsQuery));
}
[Fact]
public void AddOrganizationIntegrationConfigurationCommandsQueries_MultipleCalls_IsIdempotent()
{
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
var createCmdDescriptors = _services.Where(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand)).ToList();
Assert.Single(createCmdDescriptors);
}
private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data) private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data)
{ {
var config = new ConfigurationBuilder() var config = new ConfigurationBuilder()

View File

@@ -0,0 +1,179 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class CreateOrganizationIntegrationConfigurationCommandTests
{
[Theory, BitAutoData]
public async Task CreateAsync_Success_CreatesConfigurationAndInvalidatesCache(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = EventType.User_LoggedIn;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(configuration)
.Returns(configuration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
configuration.EventType.Value));
// Also verify RemoveByTagAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
Assert.Equal(configuration, result);
}
[Theory, BitAutoData]
public async Task CreateAsync_WildcardSuccess_CreatesConfigurationAndInvalidatesCache(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = null;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(configuration)
.Returns(configuration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
// Also verify RemoveAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
Assert.Equal(configuration, result);
}
[Theory, BitAutoData]
public async Task CreateAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegrationConfiguration configuration)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task CreateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task CreateAsync_ValidationFails_ThrowsBadRequest(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(false);
integration.Id = integrationId;
integration.OrganizationId = organizationId;
configuration.OrganizationIntegrationId = integrationId;
configuration.Template = "template";
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
}

View File

@@ -0,0 +1,211 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class DeleteOrganizationIntegrationConfigurationCommandTests
{
[Theory, BitAutoData]
public async Task DeleteAsync_Success_DeletesConfigurationAndInvalidatesCache(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.Id = configurationId;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = EventType.User_LoggedIn;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(configuration);
await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.DeleteAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
configuration.EventType.Value));
// Also verify RemoveByTagAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_WildcardSuccess_DeletesConfigurationAndInvalidatesCache(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.Id = configurationId;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = null;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(configuration);
await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.DeleteAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
// Also verify RemoveAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_ConfigurationDoesNotExist_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns((OrganizationIntegrationConfiguration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_ConfigurationDoesNotBelongToIntegration_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
configuration.Id = configurationId;
configuration.OrganizationIntegrationId = Guid.NewGuid(); // Different integration
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(configuration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
}

View File

@@ -0,0 +1,101 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class GetOrganizationIntegrationConfigurationsQueryTests
{
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_Success_ReturnsConfigurations(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
List<OrganizationIntegrationConfiguration> configurations)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(integrationId)
.Returns(configurations);
var result = await sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(integrationId);
Assert.Equal(configurations.Count, result.Count);
}
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_NoConfigurations_ReturnsEmptyList(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(integrationId)
.Returns([]);
var result = await sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId);
Assert.Empty(result);
}
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetManyByIntegrationAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetManyByIntegrationAsync(Arg.Any<Guid>());
}
}

View File

@@ -0,0 +1,390 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class UpdateOrganizationIntegrationConfigurationCommandTests
{
[Theory, BitAutoData]
public async Task UpdateAsync_Success_UpdatesConfigurationAndInvalidatesCache(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
existingConfiguration.EventType.Value));
// Also verify RemoveByTagAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
Assert.Equal(updatedConfiguration, result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_WildcardSuccess_UpdatesConfigurationAndInvalidatesCache(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = null;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.EventType = null;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
// Also verify RemoveAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
Assert.Equal(updatedConfiguration, result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_ChangedEventType_UpdatesConfigurationAndInvalidatesCacheForBothTypes(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.EventType = EventType.Cipher_Created;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
existingConfiguration.EventType.Value));
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
updatedConfiguration.EventType.Value));
// Verify RemoveByTagAsync was NOT called since both are specific event types
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
Assert.Equal(updatedConfiguration, result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegrationConfiguration updatedConfiguration)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ConfigurationDoesNotExist_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns((OrganizationIntegrationConfiguration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ConfigurationDoesNotBelongToIntegration_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = Guid.NewGuid(); // Different integration
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ValidationFails_ThrowsBadRequest(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(false);
integration.Id = integrationId;
integration.OrganizationId = organizationId;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.Template = "template";
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ChangedFromWildcardToSpecific_InvalidatesAllCaches(
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration,
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider)
{
integration.OrganizationId = organizationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = null; // Wildcard
updatedConfiguration.EventType = EventType.User_LoggedIn; // Specific
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId).Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(existingConfiguration.Id).Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
await sutProvider.Sut.UpdateAsync(organizationId, integrationId, existingConfiguration.Id, updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ChangedFromSpecificToWildcard_InvalidatesAllCaches(
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration,
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider)
{
integration.OrganizationId = organizationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn; // Specific
updatedConfiguration.EventType = null; // Wildcard
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId).Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(existingConfiguration.Id).Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
await sutProvider.Sut.UpdateAsync(organizationId, integrationId, existingConfiguration.Id, updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
}
}

View File

@@ -0,0 +1,244 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Services;
public class OrganizationIntegrationConfigurationValidatorTests
{
private readonly OrganizationIntegrationConfigurationValidator _sut = new();
[Fact]
public void ValidateConfiguration_CloudBillingSyncIntegration_ReturnsFalse()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = "{}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.CloudBillingSync, configuration));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ValidateConfiguration_EmptyTemplate_ReturnsFalse(string? template)
{
var config1 = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration(ChannelId: "C12345")),
Template = template
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Slack, config1));
var config2 = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://example.com"))),
Template = template
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, config2));
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void ValidateConfiguration_EmptyNonNullConfiguration_ReturnsFalse(string? config)
{
var config1 = new OrganizationIntegrationConfiguration
{
Configuration = config,
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Hec, config1));
var config2 = new OrganizationIntegrationConfiguration
{
Configuration = config,
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Datadog, config2));
var config3 = new OrganizationIntegrationConfiguration
{
Configuration = config,
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Teams, config3));
}
[Fact]
public void ValidateConfiguration_NullConfiguration_ReturnsTrue()
{
var config1 = new OrganizationIntegrationConfiguration
{
Configuration = null,
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Hec, config1));
var config2 = new OrganizationIntegrationConfiguration
{
Configuration = null,
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Datadog, config2));
var config3 = new OrganizationIntegrationConfiguration
{
Configuration = null,
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Teams, config3));
}
[Fact]
public void ValidateConfiguration_InvalidJsonConfiguration_ReturnsFalse()
{
var config = new OrganizationIntegrationConfiguration
{
Configuration = "{not valid json}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Slack, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Hec, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Datadog, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Teams, config));
}
[Fact]
public void ValidateConfiguration_InvalidJsonFilters_ReturnsFalse()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://example.com"))),
Template = "template",
Filters = "{Not valid json}"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_ScimIntegration_ReturnsFalse()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = "{}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Scim, configuration));
}
[Fact]
public void ValidateConfiguration_ValidSlackConfiguration_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration(ChannelId: "C12345")),
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Slack, configuration));
}
[Fact]
public void ValidateConfiguration_ValidSlackConfigurationWithFilters_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345")),
Template = "template",
Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
Rules = [
new IntegrationFilterRule()
{
Operation = IntegrationFilterOperation.Equals,
Property = "CollectionId",
Value = Guid.NewGuid()
}
],
Groups = []
})
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Slack, configuration));
}
[Fact]
public void ValidateConfiguration_ValidNoAuthWebhookConfiguration_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"))),
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_ValidWebhookConfiguration_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN")),
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_ValidWebhookConfigurationWithFilters_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(
Uri: new Uri("https://example.com"),
Scheme: "Bearer",
Token: "AUTH-TOKEN")),
Template = "template",
Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
Rules = [
new IntegrationFilterRule()
{
Operation = IntegrationFilterOperation.Equals,
Property = "CollectionId",
Value = Guid.NewGuid()
}
],
Groups = []
})
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_UnknownIntegrationType_ReturnsFalse()
{
var unknownType = (IntegrationType)999;
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = "{}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(unknownType, configuration));
}
}

View File

@@ -55,20 +55,6 @@ public class EventIntegrationsCacheConstantsTests
Assert.NotEqual(keyWithEvent, keyWithDifferentIntegration); Assert.NotEqual(keyWithEvent, keyWithDifferentIntegration);
Assert.NotEqual(keyWithEvent, keyWithDifferentOrganization); Assert.NotEqual(keyWithEvent, keyWithDifferentOrganization);
Assert.Equal(keyWithEvent, keyWithSameDetails); 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] [Theory, BitAutoData]