#nullable enable using Bit.Api.AdminConsole.Controllers; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; namespace Bit.Api.Test.AdminConsole.Controllers; [ControllerCustomize(typeof(SlackIntegrationController))] [SutProviderCustomize] public class SlackIntegrationControllerTests { private const string _slackToken = "xoxb-test-token"; private const string _validSlackCode = "A_test_code"; [Theory, BitAutoData] public async Task CreateAsync_AllParamsProvided_Succeeds( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Slack; integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) .Returns(_slackToken); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); var requestAction = await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()); await sutProvider.GetDependency().Received(1) .UpsertAsync(Arg.Any()); Assert.IsType(requestAction); } [Theory, BitAutoData] public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Slack; integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(string.Empty, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_CallbackUrlIsEmpty_ThrowsBadRequest( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Slack; integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns((string?)null); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_SlackServiceReturnsEmpty_ThrowsBadRequest( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Slack; integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) .Returns(string.Empty); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_StateEmpty_ThrowsNotFound( SutProvider sutProvider) { sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) .Returns(_slackToken); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, string.Empty)); } [Theory, BitAutoData] public async Task CreateAsync_StateExpired_ThrowsNotFound( SutProvider sutProvider, OrganizationIntegration integration) { var timeProvider = new FakeTimeProvider(new DateTime(2024, 4, 3, 2, 1, 0, DateTimeKind.Utc)); sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) .Returns(_slackToken); var state = IntegrationOAuthState.FromIntegration(integration, timeProvider); timeProvider.Advance(TimeSpan.FromMinutes(30)); sutProvider.SetDependency(timeProvider); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_StateHasNonexistentIntegration_ThrowsNotFound( SutProvider sutProvider, OrganizationIntegration integration) { sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) .Returns(_slackToken); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound( SutProvider sutProvider, OrganizationIntegration integration, OrganizationIntegration wrongOrgIntegration) { wrongOrgIntegration.Id = integration.Id; wrongOrgIntegration.Type = IntegrationType.Slack; wrongOrgIntegration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) .Returns(_slackToken); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(wrongOrgIntegration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_StateHasNonEmptyIntegration_ThrowsNotFound( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Slack; integration.Configuration = "{}"; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) .Returns(_slackToken); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_StateHasNonSlackIntegration_ThrowsNotFound( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Hec; integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) .Returns(_slackToken); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString())); } [Theory, BitAutoData] public async Task RedirectAsync_Success( SutProvider sutProvider, OrganizationIntegration integration) { integration.Configuration = null; var expectedUrl = "https://localhost/"; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns(expectedUrl); sutProvider.GetDependency() .OrganizationOwner(integration.OrganizationId) .Returns(true); sutProvider.GetDependency() .GetManyByOrganizationAsync(integration.OrganizationId) .Returns([]); sutProvider.GetDependency() .CreateAsync(Arg.Any()) .Returns(integration); sutProvider.GetDependency().GetRedirectUrl(Arg.Any(), Arg.Any()).Returns(expectedUrl); var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); var requestAction = await sutProvider.Sut.RedirectAsync(integration.OrganizationId); Assert.IsType(requestAction); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Any()); sutProvider.GetDependency().Received(1).GetRedirectUrl(Arg.Any(), expectedState.ToString()); } [Theory, BitAutoData] public async Task RedirectAsync_IntegrationAlreadyExistsWithNullConfig_Success( SutProvider sutProvider, Guid organizationId, OrganizationIntegration integration) { integration.OrganizationId = organizationId; integration.Configuration = null; integration.Type = IntegrationType.Slack; var expectedUrl = "https://localhost/"; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns(expectedUrl); sutProvider.GetDependency() .OrganizationOwner(organizationId) .Returns(true); sutProvider.GetDependency() .GetManyByOrganizationAsync(organizationId) .Returns([integration]); sutProvider.GetDependency().GetRedirectUrl(Arg.Any(), Arg.Any()).Returns(expectedUrl); var requestAction = await sutProvider.Sut.RedirectAsync(organizationId); var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); Assert.IsType(requestAction); sutProvider.GetDependency().Received(1).GetRedirectUrl(Arg.Any(), expectedState.ToString()); } [Theory, BitAutoData] public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest( SutProvider sutProvider, Guid organizationId, OrganizationIntegration integration) { integration.OrganizationId = organizationId; integration.Configuration = "{}"; integration.Type = IntegrationType.Slack; var expectedUrl = "https://localhost/"; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns(expectedUrl); sutProvider.GetDependency() .OrganizationOwner(organizationId) .Returns(true); sutProvider.GetDependency() .GetManyByOrganizationAsync(organizationId) .Returns([integration]); sutProvider.GetDependency().GetRedirectUrl(Arg.Any(), Arg.Any()).Returns(expectedUrl); await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId)); } [Theory, BitAutoData] public async Task RedirectAsync_CallbackUrlReturnsEmpty_ThrowsBadRequest( SutProvider sutProvider, Guid organizationId) { sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns((string?)null); sutProvider.GetDependency() .OrganizationOwner(organizationId) .Returns(true); await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId)); } [Theory, BitAutoData] public async Task RedirectAsync_SlackServiceReturnsEmpty_ThrowsNotFound( SutProvider sutProvider, Guid organizationId, OrganizationIntegration integration) { integration.OrganizationId = organizationId; integration.Configuration = null; var expectedUrl = "https://localhost/"; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns(expectedUrl); sutProvider.GetDependency() .OrganizationOwner(organizationId) .Returns(true); sutProvider.GetDependency() .GetManyByOrganizationAsync(organizationId) .Returns([]); sutProvider.GetDependency() .CreateAsync(Arg.Any()) .Returns(integration); sutProvider.GetDependency().GetRedirectUrl(Arg.Any(), Arg.Any()).Returns(string.Empty); await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId)); } [Theory, BitAutoData] public async Task RedirectAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider sutProvider, Guid organizationId) { sutProvider.GetDependency() .OrganizationOwner(organizationId) .Returns(false); await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId)); } }