#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.Models.Teams; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; namespace Bit.Api.Test.AdminConsole.Controllers; [ControllerCustomize(typeof(TeamsIntegrationController))] [SutProviderCustomize] public class TeamsIntegrationControllerTests { private const string _teamsToken = "test-token"; private const string _validTeamsCode = "A_test_code"; [Theory, BitAutoData] public async Task CreateAsync_AllParamsProvided_Succeeds( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Teams; integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) .Returns(_teamsToken); sutProvider.GetDependency() .GetJoinedTeamsAsync(_teamsToken) .Returns([ new TeamInfo() { DisplayName = "Test Team", Id = Guid.NewGuid().ToString(), TenantId = Guid.NewGuid().ToString() } ]); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); var requestAction = await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()); await sutProvider.GetDependency().Received(1) .UpsertAsync(Arg.Any()); Assert.IsType(requestAction); } [Theory, BitAutoData] public async Task CreateAsync_CallbackUrlIsEmpty_ThrowsBadRequest( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Teams; integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_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(_validTeamsCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Teams; integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_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_NoTeamsFound_ThrowsBadRequest( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Teams; integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) .Returns(_teamsToken); sutProvider.GetDependency() .GetJoinedTeamsAsync(_teamsToken) .Returns([]); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_TeamsServiceReturnsEmptyToken_ThrowsBadRequest( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Teams; integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) .Returns(string.Empty); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, 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 == "TeamsIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) .Returns(_teamsToken); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, 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 == "TeamsIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) .Returns(_teamsToken); var state = IntegrationOAuthState.FromIntegration(integration, timeProvider); timeProvider.Advance(TimeSpan.FromMinutes(30)); sutProvider.SetDependency(timeProvider); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, 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 == "TeamsIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) .Returns(_teamsToken); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound( SutProvider sutProvider, OrganizationIntegration integration, OrganizationIntegration wrongOrgIntegration) { wrongOrgIntegration.Id = integration.Id; wrongOrgIntegration.Type = IntegrationType.Teams; wrongOrgIntegration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) .Returns(_teamsToken); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(wrongOrgIntegration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_StateHasNonEmptyIntegration_ThrowsNotFound( SutProvider sutProvider, OrganizationIntegration integration) { integration.Type = IntegrationType.Teams; integration.Configuration = "{}"; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) .Returns(_teamsToken); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); } [Theory, BitAutoData] public async Task CreateAsync_StateHasNonTeamsIntegration_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 == "TeamsIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) .Returns(_teamsToken); sutProvider.GetDependency() .GetByIdAsync(integration.Id) .Returns(integration); var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, 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 == "TeamsIntegration_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.Teams; var expectedUrl = "https://localhost/"; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_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_CallbackUrlIsEmpty_ThrowsBadRequest( SutProvider sutProvider, Guid organizationId, OrganizationIntegration integration) { integration.OrganizationId = organizationId; integration.Configuration = null; integration.Type = IntegrationType.Teams; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) .Returns((string?)null); sutProvider.GetDependency() .OrganizationOwner(organizationId) .Returns(true); sutProvider.GetDependency() .GetManyByOrganizationAsync(organizationId) .Returns([integration]); await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId)); } [Theory, BitAutoData] public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest( SutProvider sutProvider, Guid organizationId, OrganizationIntegration integration) { integration.OrganizationId = organizationId; integration.Configuration = "{}"; integration.Type = IntegrationType.Teams; var expectedUrl = "https://localhost/"; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_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_TeamsServiceReturnsEmpty_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 == "TeamsIntegration_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)); } [Theory, BitAutoData] public async Task IncomingPostAsync_ForwardsToBot(SutProvider sutProvider) { var adapter = sutProvider.GetDependency(); var bot = sutProvider.GetDependency(); await sutProvider.Sut.IncomingPostAsync(); await adapter.Received(1).ProcessAsync(Arg.Any(), Arg.Any(), bot); } }