#nullable enable using System.Net; using System.Text.Json; using System.Web; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; 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 Bit.Test.Common.MockedHttpClient; using NSubstitute; using Xunit; using GlobalSettings = Bit.Core.Settings.GlobalSettings; namespace Bit.Core.Test.Services; [SutProviderCustomize] public class TeamsServiceTests { private readonly MockedHttpMessageHandler _handler; private readonly HttpClient _httpClient; public TeamsServiceTests() { _handler = new MockedHttpMessageHandler(); _httpClient = _handler.ToHttpClient(); } private SutProvider GetSutProvider() { var clientFactory = Substitute.For(); clientFactory.CreateClient(TeamsService.HttpClientName).Returns(_httpClient); var globalSettings = Substitute.For(); globalSettings.Teams.LoginBaseUrl.Returns("https://login.example.com"); globalSettings.Teams.GraphBaseUrl.Returns("https://graph.example.com"); return new SutProvider() .SetDependency(clientFactory) .SetDependency(globalSettings) .Create(); } [Fact] public void GetRedirectUrl_ReturnsCorrectUrl() { var sutProvider = GetSutProvider(); var clientId = sutProvider.GetDependency().Teams.ClientId; var scopes = sutProvider.GetDependency().Teams.Scopes; var callbackUrl = "https://example.com/callback"; var state = Guid.NewGuid().ToString(); var result = sutProvider.Sut.GetRedirectUrl(callbackUrl, state); var uri = new Uri(result); var query = HttpUtility.ParseQueryString(uri.Query); Assert.Equal(clientId, query["client_id"]); Assert.Equal(scopes, query["scope"]); Assert.Equal(callbackUrl, query["redirect_uri"]); Assert.Equal(state, query["state"]); Assert.Equal("login.example.com", uri.Host); Assert.Equal("/common/oauth2/v2.0/authorize", uri.AbsolutePath); } [Fact] public async Task ObtainTokenViaOAuth_Success_ReturnsAccessToken() { var sutProvider = GetSutProvider(); var jsonResponse = JsonSerializer.Serialize(new { access_token = "test-access-token" }); _handler.When("https://login.example.com/common/oauth2/v2.0/token") .RespondWith(HttpStatusCode.OK) .WithContent(new StringContent(jsonResponse)); var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback"); Assert.Equal("test-access-token", result); } [Theory] [InlineData("test-code", "")] [InlineData("", "https://example.com/callback")] [InlineData("", "")] public async Task ObtainTokenViaOAuth_CodeOrRedirectUrlIsEmpty_ReturnsEmptyString(string code, string redirectUrl) { var sutProvider = GetSutProvider(); var result = await sutProvider.Sut.ObtainTokenViaOAuth(code, redirectUrl); Assert.Equal(string.Empty, result); } [Fact] public async Task ObtainTokenViaOAuth_HttpFailure_ReturnsEmptyString() { var sutProvider = GetSutProvider(); _handler.When("https://login.example.com/common/oauth2/v2.0/token") .RespondWith(HttpStatusCode.InternalServerError) .WithContent(new StringContent(string.Empty)); var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback"); Assert.Equal(string.Empty, result); } [Fact] public async Task ObtainTokenViaOAuth_UnknownResponse_ReturnsEmptyString() { var sutProvider = GetSutProvider(); _handler.When("https://login.example.com/common/oauth2/v2.0/token") .RespondWith(HttpStatusCode.OK) .WithContent(new StringContent("Not an expected response")); var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback"); Assert.Equal(string.Empty, result); } [Fact] public async Task GetJoinedTeamsAsync_Success_ReturnsTeams() { var sutProvider = GetSutProvider(); var jsonResponse = JsonSerializer.Serialize(new { value = new[] { new { id = "team1", displayName = "Team One" }, new { id = "team2", displayName = "Team Two" } } }); _handler.When("https://graph.example.com/me/joinedTeams") .RespondWith(HttpStatusCode.OK) .WithContent(new StringContent(jsonResponse)); var result = await sutProvider.Sut.GetJoinedTeamsAsync("fake-access-token"); Assert.Equal(2, result.Count); Assert.Contains(result, t => t is { Id: "team1", DisplayName: "Team One" }); Assert.Contains(result, t => t is { Id: "team2", DisplayName: "Team Two" }); } [Fact] public async Task GetJoinedTeamsAsync_ServerReturnsEmpty_ReturnsEmptyList() { var sutProvider = GetSutProvider(); var jsonResponse = JsonSerializer.Serialize(new { value = (object?)null }); _handler.When("https://graph.example.com/me/joinedTeams") .RespondWith(HttpStatusCode.OK) .WithContent(new StringContent(jsonResponse)); var result = await sutProvider.Sut.GetJoinedTeamsAsync("fake-access-token"); Assert.NotNull(result); Assert.Empty(result); } [Fact] public async Task GetJoinedTeamsAsync_ServerErrorCode_ReturnsEmptyList() { var sutProvider = GetSutProvider(); _handler.When("https://graph.example.com/me/joinedTeams") .RespondWith(HttpStatusCode.Unauthorized) .WithContent(new StringContent("Unauthorized")); var result = await sutProvider.Sut.GetJoinedTeamsAsync("fake-access-token"); Assert.NotNull(result); Assert.Empty(result); } [Theory, BitAutoData] public async Task HandleIncomingAppInstall_Success_UpdatesTeamsIntegration( OrganizationIntegration integration) { var sutProvider = GetSutProvider(); var tenantId = Guid.NewGuid().ToString(); var teamId = Guid.NewGuid().ToString(); var conversationId = Guid.NewGuid().ToString(); var serviceUrl = new Uri("https://localhost"); var initiatedConfiguration = new TeamsIntegration(TenantId: tenantId, Teams: [ new TeamInfo() { Id = teamId, DisplayName = "test team", TenantId = tenantId }, new TeamInfo() { Id = Guid.NewGuid().ToString(), DisplayName = "other team", TenantId = tenantId }, new TeamInfo() { Id = Guid.NewGuid().ToString(), DisplayName = "third team", TenantId = tenantId } ]); integration.Configuration = JsonSerializer.Serialize(initiatedConfiguration); sutProvider.GetDependency() .GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId) .Returns(integration); OrganizationIntegration? capturedIntegration = null; await sutProvider.GetDependency() .UpsertAsync(Arg.Do(x => capturedIntegration = x)); await sutProvider.Sut.HandleIncomingAppInstallAsync( conversationId: conversationId, serviceUrl: serviceUrl, teamId: teamId, tenantId: tenantId ); await sutProvider.GetDependency().Received(1).GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId); Assert.NotNull(capturedIntegration); var configuration = JsonSerializer.Deserialize(capturedIntegration.Configuration ?? string.Empty); Assert.NotNull(configuration); Assert.NotNull(configuration.ServiceUrl); Assert.Equal(serviceUrl, configuration.ServiceUrl); Assert.Equal(conversationId, configuration.ChannelId); } [Fact] public async Task HandleIncomingAppInstall_NoIntegrationMatched_DoesNothing() { var sutProvider = GetSutProvider(); await sutProvider.Sut.HandleIncomingAppInstallAsync( conversationId: "conversationId", serviceUrl: new Uri("https://localhost"), teamId: "teamId", tenantId: "tenantId" ); await sutProvider.GetDependency().Received(1).GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId"); await sutProvider.GetDependency().DidNotReceive().UpsertAsync(Arg.Any()); } [Theory, BitAutoData] public async Task HandleIncomingAppInstall_MatchedIntegrationAlreadySetup_DoesNothing( OrganizationIntegration integration) { var sutProvider = GetSutProvider(); var tenantId = Guid.NewGuid().ToString(); var teamId = Guid.NewGuid().ToString(); var initiatedConfiguration = new TeamsIntegration( TenantId: tenantId, Teams: [new TeamInfo() { Id = teamId, DisplayName = "test team", TenantId = tenantId }], ChannelId: "ChannelId", ServiceUrl: new Uri("https://localhost") ); integration.Configuration = JsonSerializer.Serialize(initiatedConfiguration); sutProvider.GetDependency() .GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId) .Returns(integration); await sutProvider.Sut.HandleIncomingAppInstallAsync( conversationId: "conversationId", serviceUrl: new Uri("https://localhost"), teamId: teamId, tenantId: tenantId ); await sutProvider.GetDependency().Received(1).GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId); await sutProvider.GetDependency().DidNotReceive().UpsertAsync(Arg.Any()); } [Theory, BitAutoData] public async Task HandleIncomingAppInstall_MatchedIntegrationWithMissingConfiguration_DoesNothing( OrganizationIntegration integration) { var sutProvider = GetSutProvider(); integration.Configuration = null; sutProvider.GetDependency() .GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId") .Returns(integration); await sutProvider.Sut.HandleIncomingAppInstallAsync( conversationId: "conversationId", serviceUrl: new Uri("https://localhost"), teamId: "teamId", tenantId: "tenantId" ); await sutProvider.GetDependency().Received(1).GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId"); await sutProvider.GetDependency().DidNotReceive().UpsertAsync(Arg.Any()); } }