mirror of
https://github.com/bitwarden/server
synced 2026-01-07 19:13:50 +00:00
Add Microsoft Teams integration (#6410)
* Add Microsoft Teams integration * Fix method naming error * Expand and clean up unit test coverage * Update with PR feedback * Add documentation, add In Progress logic/tests for Teams * Fixed lowercase Slack * Added docs; Updated PR suggestions; * Fix broken tests
This commit is contained in:
@@ -34,7 +34,7 @@ public class SlackIntegrationControllerTests
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
@@ -60,7 +60,7 @@ public class SlackIntegrationControllerTests
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
@@ -80,7 +80,7 @@ public class SlackIntegrationControllerTests
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
@@ -99,13 +99,13 @@ public class SlackIntegrationControllerTests
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
.Returns(_slackToken);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, String.Empty));
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, string.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -116,7 +116,7 @@ public class SlackIntegrationControllerTests
|
||||
var timeProvider = new FakeTimeProvider(new DateTime(2024, 4, 3, 2, 1, 0, DateTimeKind.Utc));
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
@@ -135,7 +135,7 @@ public class SlackIntegrationControllerTests
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
@@ -147,7 +147,7 @@ public class SlackIntegrationControllerTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasWrongOgranizationHash_ThrowsNotFound(
|
||||
public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration,
|
||||
OrganizationIntegration wrongOrgIntegration)
|
||||
@@ -156,7 +156,7 @@ public class SlackIntegrationControllerTests
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
@@ -179,7 +179,7 @@ public class SlackIntegrationControllerTests
|
||||
integration.Configuration = "{}";
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
@@ -201,7 +201,7 @@ public class SlackIntegrationControllerTests
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
@@ -224,7 +224,7 @@ public class SlackIntegrationControllerTests
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(integration.OrganizationId)
|
||||
@@ -260,7 +260,7 @@ public class SlackIntegrationControllerTests
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
@@ -291,7 +291,7 @@ public class SlackIntegrationControllerTests
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
@@ -316,7 +316,7 @@ public class SlackIntegrationControllerTests
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
#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<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.GetJoinedTeamsAsync(_teamsToken)
|
||||
.Returns([
|
||||
new TeamInfo() { DisplayName = "Test Team", Id = Guid.NewGuid().ToString(), TenantId = Guid.NewGuid().ToString() }
|
||||
]);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString());
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.UpsertAsync(Arg.Any<OrganizationIntegration>());
|
||||
Assert.IsType<CreatedResult>(requestAction);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.CreateAsync(string.Empty, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_NoTeamsFound_ThrowsBadRequest(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.GetJoinedTeamsAsync(_teamsToken)
|
||||
.Returns([]);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_TeamsServiceReturnsEmptyToken_ThrowsBadRequest(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(string.Empty);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateEmpty_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, string.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateExpired_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(new DateTime(2024, 4, 3, 2, 1, 0, DateTimeKind.Utc));
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);
|
||||
timeProvider.Advance(TimeSpan.FromMinutes(30));
|
||||
|
||||
sutProvider.SetDependency<TimeProvider>(timeProvider);
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasNonexistentIntegration_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration,
|
||||
OrganizationIntegration wrongOrgIntegration)
|
||||
{
|
||||
wrongOrgIntegration.Id = integration.Id;
|
||||
wrongOrgIntegration.Type = IntegrationType.Teams;
|
||||
wrongOrgIntegration.Configuration = null;
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(wrongOrgIntegration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasNonEmptyIntegration_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = "{}";
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasNonTeamsIntegration_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Hec;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_Success(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Configuration = null;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(integration.OrganizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(integration.OrganizationId)
|
||||
.Returns([]);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
||||
.Returns(integration);
|
||||
sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
|
||||
|
||||
var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
var requestAction = await sutProvider.Sut.RedirectAsync(integration.OrganizationId);
|
||||
|
||||
Assert.IsType<RedirectResult>(requestAction);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>());
|
||||
sutProvider.GetDependency<ITeamsService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_IntegrationAlreadyExistsWithNullConfig_Success(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.OrganizationId = organizationId;
|
||||
integration.Configuration = null;
|
||||
integration.Type = IntegrationType.Teams;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(organizationId)
|
||||
.Returns([integration]);
|
||||
sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
|
||||
|
||||
var requestAction = await sutProvider.Sut.RedirectAsync(organizationId);
|
||||
|
||||
var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
Assert.IsType<RedirectResult>(requestAction);
|
||||
sutProvider.GetDependency<ITeamsService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.OrganizationId = organizationId;
|
||||
integration.Configuration = "{}";
|
||||
integration.Type = IntegrationType.Teams;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(organizationId)
|
||||
.Returns([integration]);
|
||||
sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_TeamsServiceReturnsEmpty_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.OrganizationId = organizationId;
|
||||
integration.Configuration = null;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(organizationId)
|
||||
.Returns([]);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
||||
.Returns(integration);
|
||||
sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(string.Empty);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<TeamsIntegrationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IncomingPostAsync_ForwardsToBot(SutProvider<TeamsIntegrationController> sutProvider)
|
||||
{
|
||||
var adapter = sutProvider.GetDependency<IBotFrameworkHttpAdapter>();
|
||||
var bot = sutProvider.GetDependency<IBot>();
|
||||
|
||||
await sutProvider.Sut.IncomingPostAsync();
|
||||
await adapter.Received(1).ProcessAsync(Arg.Any<HttpRequest>(), Arg.Any<HttpResponse>(), bot);
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
[Theory]
|
||||
[InlineData(data: "")]
|
||||
[InlineData(data: " ")]
|
||||
public void IsValidForType_EmptyNonNullHecConfiguration_ReturnsFalse(string? config)
|
||||
public void IsValidForType_EmptyNonNullConfiguration_ReturnsFalse(string? config)
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
@@ -48,10 +48,12 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
};
|
||||
|
||||
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_NullHecConfiguration_ReturnsTrue()
|
||||
public void IsValidForType_NullConfiguration_ReturnsTrue()
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
@@ -60,32 +62,8 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
};
|
||||
|
||||
Assert.True(condition: model.IsValidForType(IntegrationType.Hec));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(data: "")]
|
||||
[InlineData(data: " ")]
|
||||
public void IsValidForType_EmptyNonNullDatadogConfiguration_ReturnsFalse(string? config)
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = config,
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Datadog));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForType_NullDatadogConfiguration_ReturnsTrue()
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = null,
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
Assert.True(condition: model.IsValidForType(IntegrationType.Datadog));
|
||||
Assert.True(condition: model.IsValidForType(IntegrationType.Teams));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -107,6 +85,8 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
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]
|
||||
@@ -121,6 +101,8 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -57,6 +57,22 @@ public class OrganizationIntegrationRequestModelTests
|
||||
Assert.Contains("cannot be created directly", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Teams_ReturnsCannotBeCreatedDirectlyError()
|
||||
{
|
||||
var model = new OrganizationIntegrationRequestModel
|
||||
{
|
||||
Type = IntegrationType.Teams,
|
||||
Configuration = null
|
||||
};
|
||||
|
||||
var results = model.Validate(new ValidationContext(model)).ToList();
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Contains(nameof(model.Type), results[0].MemberNames);
|
||||
Assert.Contains("cannot be created directly", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Webhook_WithNullConfiguration_ReturnsNoErrors()
|
||||
{
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Teams;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
@@ -58,6 +61,46 @@ public class OrganizationIntegrationResponseModelTests
|
||||
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Teams_NullConfig_ReturnsInitiated(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Teams;
|
||||
oi.Configuration = null;
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.Initiated, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Teams_WithTenantAndTeamsConfig_ReturnsInProgress(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Teams;
|
||||
oi.Configuration = JsonSerializer.Serialize(new TeamsIntegration(
|
||||
TenantId: "tenant", Teams: [new TeamInfo() { DisplayName = "Team", Id = "TeamId", TenantId = "tenant" }]
|
||||
));
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.InProgress, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Teams_WithCompletedConfig_ReturnsCompleted(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Teams;
|
||||
oi.Configuration = JsonSerializer.Serialize(new TeamsIntegration(
|
||||
TenantId: "tenant",
|
||||
Teams: [new TeamInfo() { DisplayName = "Team", Id = "TeamId", TenantId = "tenant" }],
|
||||
ServiceUrl: new Uri("https://example.com"),
|
||||
ChannelId: "channellId"
|
||||
));
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Webhook_AlwaysCompleted(OrganizationIntegration oi)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Bit.Core.AdminConsole.Models.Teams;
|
||||
using Microsoft.Bot.Connector.Authentication;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Data.Teams;
|
||||
|
||||
public class TeamsBotCredentialProviderTests
|
||||
{
|
||||
private string _clientId = "client id";
|
||||
private string _clientSecret = "client secret";
|
||||
|
||||
[Fact]
|
||||
public async Task IsValidAppId_MustMatchClientId()
|
||||
{
|
||||
var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);
|
||||
|
||||
Assert.True(await sut.IsValidAppIdAsync(_clientId));
|
||||
Assert.False(await sut.IsValidAppIdAsync("Different id"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAppPasswordAsync_MatchingClientId_ReturnsClientSecret()
|
||||
{
|
||||
var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);
|
||||
var password = await sut.GetAppPasswordAsync(_clientId);
|
||||
Assert.Equal(_clientSecret, password);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAppPasswordAsync_NotMatchingClientId_ReturnsNull()
|
||||
{
|
||||
var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);
|
||||
Assert.Null(await sut.GetAppPasswordAsync("Different id"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsAuthenticationDisabledAsync_ReturnsFalse()
|
||||
{
|
||||
var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);
|
||||
Assert.False(await sut.IsAuthenticationDisabledAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateIssuerAsync_ExpectedIssuer_ReturnsTrue()
|
||||
{
|
||||
var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);
|
||||
Assert.True(await sut.ValidateIssuerAsync(AuthenticationConstants.ToBotFromChannelTokenIssuer));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateIssuerAsync_UnexpectedIssuer_ReturnsFalse()
|
||||
{
|
||||
var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);
|
||||
Assert.False(await sut.ValidateIssuerAsync("unexpected issuer"));
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,6 @@ namespace Bit.Core.Test.Services;
|
||||
|
||||
public class IntegrationTypeTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToRoutingKey_Slack_Succeeds()
|
||||
{
|
||||
Assert.Equal("slack", IntegrationType.Slack.ToRoutingKey());
|
||||
}
|
||||
[Fact]
|
||||
public void ToRoutingKey_Webhook_Succeeds()
|
||||
{
|
||||
Assert.Equal("webhook", IntegrationType.Webhook.ToRoutingKey());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRoutingKey_CloudBillingSync_ThrowsException()
|
||||
{
|
||||
@@ -27,4 +16,34 @@ public class IntegrationTypeTests
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => IntegrationType.Scim.ToRoutingKey());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRoutingKey_Slack_Succeeds()
|
||||
{
|
||||
Assert.Equal("slack", IntegrationType.Slack.ToRoutingKey());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRoutingKey_Webhook_Succeeds()
|
||||
{
|
||||
Assert.Equal("webhook", IntegrationType.Webhook.ToRoutingKey());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRoutingKey_Hec_Succeeds()
|
||||
{
|
||||
Assert.Equal("hec", IntegrationType.Hec.ToRoutingKey());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRoutingKey_Datadog_Succeeds()
|
||||
{
|
||||
Assert.Equal("datadog", IntegrationType.Datadog.ToRoutingKey());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRoutingKey_Teams_Succeeds()
|
||||
{
|
||||
Assert.Equal("teams", IntegrationType.Teams.ToRoutingKey());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +296,18 @@ public class SlackServiceTests
|
||||
Assert.Equal("test-access-token", result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("test-code", "")]
|
||||
[InlineData("", "https://example.com/callback")]
|
||||
[InlineData("", "")]
|
||||
public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenCodeOrRedirectUrlIsEmpty(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_ReturnsEmptyString_WhenErrorResponse()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Microsoft.Rest;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class TeamsIntegrationHandlerTests
|
||||
{
|
||||
private readonly ITeamsService _teamsService = Substitute.For<ITeamsService>();
|
||||
private readonly string _channelId = "C12345";
|
||||
private readonly Uri _serviceUrl = new Uri("http://localhost");
|
||||
|
||||
private SutProvider<TeamsIntegrationHandler> GetSutProvider()
|
||||
{
|
||||
return new SutProvider<TeamsIntegrationHandler>()
|
||||
.SetDependency(_teamsService)
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_HttpExceptionNonRetryable_ReturnsFalseAndNotRetryable(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
|
||||
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.ThrowsAsync(new HttpOperationException("Server error")
|
||||
{
|
||||
Response = new HttpResponseMessageWrapper(
|
||||
new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden),
|
||||
"Forbidden"
|
||||
)
|
||||
}
|
||||
);
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_HttpExceptionRetryable_ReturnsFalseAndRetryable(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
|
||||
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.ThrowsAsync(new HttpOperationException("Server error")
|
||||
{
|
||||
Response = new HttpResponseMessageWrapper(
|
||||
new HttpResponseMessage(System.Net.HttpStatusCode.TooManyRequests),
|
||||
"Too Many Requests"
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_UnknownException_ReturnsFalseAndNotRetryable(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
|
||||
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.ThrowsAsync(new Exception("Unknown error"));
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
|
||||
);
|
||||
}
|
||||
}
|
||||
289
test/Core.Test/AdminConsole/Services/TeamsServiceTests.cs
Normal file
289
test/Core.Test/AdminConsole/Services/TeamsServiceTests.cs
Normal file
@@ -0,0 +1,289 @@
|
||||
#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<TeamsService> GetSutProvider()
|
||||
{
|
||||
var clientFactory = Substitute.For<IHttpClientFactory>();
|
||||
clientFactory.CreateClient(TeamsService.HttpClientName).Returns(_httpClient);
|
||||
|
||||
var globalSettings = Substitute.For<GlobalSettings>();
|
||||
globalSettings.Teams.LoginBaseUrl.Returns("https://login.example.com");
|
||||
globalSettings.Teams.GraphBaseUrl.Returns("https://graph.example.com");
|
||||
|
||||
return new SutProvider<TeamsService>()
|
||||
.SetDependency(clientFactory)
|
||||
.SetDependency(globalSettings)
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRedirectUrl_ReturnsCorrectUrl()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var clientId = sutProvider.GetDependency<GlobalSettings>().Teams.ClientId;
|
||||
var scopes = sutProvider.GetDependency<GlobalSettings>().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<IOrganizationIntegrationRepository>()
|
||||
.GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId)
|
||||
.Returns(integration);
|
||||
|
||||
OrganizationIntegration? capturedIntegration = null;
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.UpsertAsync(Arg.Do<OrganizationIntegration>(x => capturedIntegration = x));
|
||||
|
||||
await sutProvider.Sut.HandleIncomingAppInstallAsync(
|
||||
conversationId: conversationId,
|
||||
serviceUrl: serviceUrl,
|
||||
teamId: teamId,
|
||||
tenantId: tenantId
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId);
|
||||
Assert.NotNull(capturedIntegration);
|
||||
var configuration = JsonSerializer.Deserialize<TeamsIntegration>(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<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId");
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive().UpsertAsync(Arg.Any<OrganizationIntegration>());
|
||||
}
|
||||
|
||||
[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<IOrganizationIntegrationRepository>()
|
||||
.GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId)
|
||||
.Returns(integration);
|
||||
|
||||
await sutProvider.Sut.HandleIncomingAppInstallAsync(
|
||||
conversationId: "conversationId",
|
||||
serviceUrl: new Uri("https://localhost"),
|
||||
teamId: teamId,
|
||||
tenantId: tenantId
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive().UpsertAsync(Arg.Any<OrganizationIntegration>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleIncomingAppInstall_MatchedIntegrationWithMissingConfiguration_DoesNothing(
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
integration.Configuration = null;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId")
|
||||
.Returns(integration);
|
||||
|
||||
await sutProvider.Sut.HandleIncomingAppInstallAsync(
|
||||
conversationId: "conversationId",
|
||||
serviceUrl: new Uri("https://localhost"),
|
||||
teamId: "teamId",
|
||||
tenantId: "tenantId"
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId");
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive().UpsertAsync(Arg.Any<OrganizationIntegration>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user