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 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() .GetByIdAsync(integrationId) .Returns(integration); sutProvider.GetDependency() .GetByIdAsync(configurationId) .Returns(existingConfiguration); sutProvider.GetDependency() .ValidateConfiguration(Arg.Any(), Arg.Any()) .Returns(true); var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration); await sutProvider.GetDependency().Received(1) .GetByIdAsync(integrationId); await sutProvider.GetDependency().Received(1) .GetByIdAsync(configurationId); await sutProvider.GetDependency().Received(1) .ReplaceAsync(updatedConfiguration); await sutProvider.GetDependency().Received(1) .RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails( organizationId, integration.Type, existingConfiguration.EventType.Value)); // Also verify RemoveByTagAsync was NOT called await sutProvider.GetDependency().DidNotReceive() .RemoveByTagAsync(Arg.Any()); Assert.Equal(updatedConfiguration, result); } [Theory, BitAutoData] public async Task UpdateAsync_WildcardSuccess_UpdatesConfigurationAndInvalidatesCache( SutProvider 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() .GetByIdAsync(integrationId) .Returns(integration); sutProvider.GetDependency() .GetByIdAsync(configurationId) .Returns(existingConfiguration); sutProvider.GetDependency() .ValidateConfiguration(Arg.Any(), Arg.Any()) .Returns(true); var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration); await sutProvider.GetDependency().Received(1) .GetByIdAsync(integrationId); await sutProvider.GetDependency().Received(1) .GetByIdAsync(configurationId); await sutProvider.GetDependency().Received(1) .ReplaceAsync(updatedConfiguration); await sutProvider.GetDependency().Received(1) .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration( organizationId, integration.Type)); // Also verify RemoveAsync was NOT called await sutProvider.GetDependency().DidNotReceive() .RemoveAsync(Arg.Any()); Assert.Equal(updatedConfiguration, result); } [Theory, BitAutoData] public async Task UpdateAsync_ChangedEventType_UpdatesConfigurationAndInvalidatesCacheForBothTypes( SutProvider 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() .GetByIdAsync(integrationId) .Returns(integration); sutProvider.GetDependency() .GetByIdAsync(configurationId) .Returns(existingConfiguration); sutProvider.GetDependency() .ValidateConfiguration(Arg.Any(), Arg.Any()) .Returns(true); var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration); await sutProvider.GetDependency().Received(1) .GetByIdAsync(integrationId); await sutProvider.GetDependency().Received(1) .GetByIdAsync(configurationId); await sutProvider.GetDependency().Received(1) .ReplaceAsync(updatedConfiguration); await sutProvider.GetDependency().Received(1) .RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails( organizationId, integration.Type, existingConfiguration.EventType.Value)); await sutProvider.GetDependency().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().DidNotReceive() .RemoveByTagAsync(Arg.Any()); Assert.Equal(updatedConfiguration, result); } [Theory, BitAutoData] public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound( SutProvider sutProvider, Guid organizationId, Guid integrationId, Guid configurationId, OrganizationIntegrationConfiguration updatedConfiguration) { sutProvider.GetDependency() .GetByIdAsync(integrationId) .Returns((OrganizationIntegration)null); await Assert.ThrowsAsync( () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration)); await sutProvider.GetDependency().DidNotReceive() .GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .ReplaceAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .RemoveAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .RemoveByTagAsync(Arg.Any()); } [Theory, BitAutoData] public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound( SutProvider sutProvider, Guid organizationId, Guid integrationId, Guid configurationId, OrganizationIntegration integration, OrganizationIntegrationConfiguration updatedConfiguration) { integration.Id = integrationId; integration.OrganizationId = Guid.NewGuid(); // Different organization sutProvider.GetDependency() .GetByIdAsync(integrationId) .Returns(integration); await Assert.ThrowsAsync( () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration)); await sutProvider.GetDependency().DidNotReceive() .GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .ReplaceAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .RemoveAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .RemoveByTagAsync(Arg.Any()); } [Theory, BitAutoData] public async Task UpdateAsync_ConfigurationDoesNotExist_ThrowsNotFound( SutProvider sutProvider, Guid organizationId, Guid integrationId, Guid configurationId, OrganizationIntegration integration, OrganizationIntegrationConfiguration updatedConfiguration) { integration.Id = integrationId; integration.OrganizationId = organizationId; sutProvider.GetDependency() .GetByIdAsync(integrationId) .Returns(integration); sutProvider.GetDependency() .GetByIdAsync(configurationId) .Returns((OrganizationIntegrationConfiguration)null); await Assert.ThrowsAsync( () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration)); await sutProvider.GetDependency().DidNotReceive() .ReplaceAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .RemoveAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .RemoveByTagAsync(Arg.Any()); } [Theory, BitAutoData] public async Task UpdateAsync_ConfigurationDoesNotBelongToIntegration_ThrowsNotFound( SutProvider 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() .GetByIdAsync(integrationId) .Returns(integration); sutProvider.GetDependency() .GetByIdAsync(configurationId) .Returns(existingConfiguration); await Assert.ThrowsAsync( () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration)); await sutProvider.GetDependency().DidNotReceive() .ReplaceAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .RemoveAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .RemoveByTagAsync(Arg.Any()); } [Theory, BitAutoData] public async Task UpdateAsync_ValidationFails_ThrowsBadRequest( SutProvider sutProvider, Guid organizationId, Guid integrationId, Guid configurationId, OrganizationIntegration integration, OrganizationIntegrationConfiguration existingConfiguration, OrganizationIntegrationConfiguration updatedConfiguration) { sutProvider.GetDependency() .ValidateConfiguration(Arg.Any(), Arg.Any()) .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() .GetByIdAsync(integrationId) .Returns(integration); sutProvider.GetDependency() .GetByIdAsync(configurationId) .Returns(existingConfiguration); await Assert.ThrowsAsync( () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration)); await sutProvider.GetDependency().DidNotReceive() .ReplaceAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .RemoveAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceive() .RemoveByTagAsync(Arg.Any()); } [Theory, BitAutoData] public async Task UpdateAsync_ChangedFromWildcardToSpecific_InvalidatesAllCaches( Guid organizationId, Guid integrationId, OrganizationIntegration integration, OrganizationIntegrationConfiguration existingConfiguration, OrganizationIntegrationConfiguration updatedConfiguration, SutProvider sutProvider) { integration.OrganizationId = organizationId; existingConfiguration.OrganizationIntegrationId = integrationId; existingConfiguration.EventType = null; // Wildcard updatedConfiguration.EventType = EventType.User_LoggedIn; // Specific sutProvider.GetDependency() .GetByIdAsync(integrationId).Returns(integration); sutProvider.GetDependency() .GetByIdAsync(existingConfiguration.Id).Returns(existingConfiguration); sutProvider.GetDependency() .ValidateConfiguration(Arg.Any(), Arg.Any()) .Returns(true); await sutProvider.Sut.UpdateAsync(organizationId, integrationId, existingConfiguration.Id, updatedConfiguration); await sutProvider.GetDependency().Received(1) .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration( organizationId, integration.Type)); await sutProvider.GetDependency().DidNotReceive() .RemoveAsync(Arg.Any()); } [Theory, BitAutoData] public async Task UpdateAsync_ChangedFromSpecificToWildcard_InvalidatesAllCaches( Guid organizationId, Guid integrationId, OrganizationIntegration integration, OrganizationIntegrationConfiguration existingConfiguration, OrganizationIntegrationConfiguration updatedConfiguration, SutProvider sutProvider) { integration.OrganizationId = organizationId; existingConfiguration.OrganizationIntegrationId = integrationId; existingConfiguration.EventType = EventType.User_LoggedIn; // Specific updatedConfiguration.EventType = null; // Wildcard sutProvider.GetDependency() .GetByIdAsync(integrationId).Returns(integration); sutProvider.GetDependency() .GetByIdAsync(existingConfiguration.Id).Returns(existingConfiguration); sutProvider.GetDependency() .ValidateConfiguration(Arg.Any(), Arg.Any()) .Returns(true); await sutProvider.Sut.UpdateAsync(organizationId, integrationId, existingConfiguration.Id, updatedConfiguration); await sutProvider.GetDependency().Received(1) .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration( organizationId, integration.Type)); await sutProvider.GetDependency().DidNotReceive() .RemoveAsync(Arg.Any()); } }