using System.Text.Json; using Bit.Api.Controllers; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response.Organizations; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; using NSubstitute; using Xunit; namespace Bit.Api.Test.Controllers; [ControllerCustomize(typeof(OrganizationConnectionsController))] [SutProviderCustomize] [JsonDocumentCustomize] public class OrganizationConnectionsControllerTests { public static IEnumerable ConnectionTypes => Enum.GetValues().Select(p => new object[] { p }); [Theory] [BitAutoData(true, true)] [BitAutoData(false, true)] [BitAutoData(true, false)] [BitAutoData(false, false)] public void ConnectionEnabled_RequiresBothSelfHostAndCommunications(bool selfHosted, bool enableCloudCommunication, SutProvider sutProvider) { var globalSettingsMock = sutProvider.GetDependency(); globalSettingsMock.SelfHosted.Returns(selfHosted); globalSettingsMock.EnableCloudCommunication.Returns(enableCloudCommunication); Action assert = selfHosted && enableCloudCommunication ? Assert.True : Assert.False; var result = sutProvider.Sut.ConnectionsEnabled(); assert(result); } [Theory] [BitAutoData] public async Task CreateConnection_CloudBillingSync_RequiresOwnerPermissions(SutProvider sutProvider) { var model = new OrganizationConnectionRequestModel { Type = OrganizationConnectionType.CloudBillingSync, }; var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateConnection(model)); Assert.Contains($"You do not have permission to create a connection of type", exception.Message); } [Theory] [BitMemberAutoData(nameof(ConnectionTypes))] public async Task CreateConnection_OnlyOneConnectionOfEachType(OrganizationConnectionType type, OrganizationConnectionRequestModel model, BillingSyncConfig config, Guid existingEntityId, SutProvider sutProvider) { model.Type = type; model.Config = JsonDocumentFromObject(config); var typedModel = new OrganizationConnectionRequestModel(model); var existing = typedModel.ToData(existingEntityId).ToEntity(); sutProvider.GetDependency().OrganizationOwner(model.OrganizationId).Returns(true); sutProvider.GetDependency().GetByOrganizationIdTypeAsync(model.OrganizationId, type).Returns(new[] { existing }); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateConnection(model)); Assert.Contains($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.", exception.Message); } [Theory] [BitAutoData] public async Task CreateConnection_BillingSyncType_InvalidLicense_Throws(OrganizationConnectionRequestModel model, BillingSyncConfig config, Guid cloudOrgId, OrganizationLicense organizationLicense, SutProvider sutProvider) { model.Type = OrganizationConnectionType.CloudBillingSync; organizationLicense.Id = cloudOrgId; model.Config = JsonDocumentFromObject(config); var typedModel = new OrganizationConnectionRequestModel(model); typedModel.ParsedConfig.CloudOrganizationId = cloudOrgId; sutProvider.GetDependency() .OrganizationOwner(model.OrganizationId) .Returns(true); sutProvider.GetDependency() .ReadOrganizationLicenseAsync(model.OrganizationId) .Returns(organizationLicense); sutProvider.GetDependency() .VerifyLicense(organizationLicense) .Returns(false); await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateConnection(model)); } [Theory] [BitAutoData] public async Task CreateConnection_Success(OrganizationConnectionRequestModel model, BillingSyncConfig config, Guid cloudOrgId, OrganizationLicense organizationLicense, SutProvider sutProvider) { organizationLicense.Id = cloudOrgId; model.Config = JsonDocumentFromObject(config); var typedModel = new OrganizationConnectionRequestModel(model); typedModel.ParsedConfig.CloudOrganizationId = cloudOrgId; sutProvider.GetDependency().SelfHosted.Returns(true); sutProvider.GetDependency().CreateAsync(default) .ReturnsForAnyArgs(typedModel.ToData(Guid.NewGuid()).ToEntity()); sutProvider.GetDependency().OrganizationOwner(model.OrganizationId).Returns(true); sutProvider.GetDependency() .ReadOrganizationLicenseAsync(Arg.Any()) .Returns(organizationLicense); sutProvider.GetDependency() .VerifyLicense(organizationLicense) .Returns(true); await sutProvider.Sut.CreateConnection(model); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(typedModel.ToData()))); } [Theory] [BitAutoData] public async Task UpdateConnection_RequiresOwnerPermissions(SutProvider sutProvider) { sutProvider.GetDependency() .GetByIdAsync(Arg.Any()) .Returns(new OrganizationConnection()); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateConnection(default, null)); Assert.Contains("You do not have permission to update this connection.", exception.Message); } [Theory] [BitAutoData(OrganizationConnectionType.CloudBillingSync)] public async Task UpdateConnection_BillingSync_OnlyOneConnectionOfEachType(OrganizationConnectionType type, OrganizationConnection existing1, OrganizationConnection existing2, BillingSyncConfig config, SutProvider sutProvider) { existing1.Type = existing2.Type = type; existing1.Config = JsonSerializer.Serialize(config); var typedModel = RequestModelFromEntity(existing1); sutProvider.GetDependency().OrganizationOwner(typedModel.OrganizationId).Returns(true); var orgConnectionRepository = sutProvider.GetDependency(); orgConnectionRepository.GetByIdAsync(existing1.Id).Returns(existing1); orgConnectionRepository.GetByIdAsync(existing2.Id).Returns(existing2); orgConnectionRepository.GetByOrganizationIdTypeAsync(typedModel.OrganizationId, type).Returns(new[] { existing1, existing2 }); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateConnection(existing1.Id, typedModel)); Assert.Contains($"The requested organization already has a connection of type {typedModel.Type}. Only one of each connection type may exist per organization.", exception.Message); } [Theory] [BitAutoData(OrganizationConnectionType.Scim)] public async Task UpdateConnection_Scim_OnlyOneConnectionOfEachType(OrganizationConnectionType type, OrganizationConnection existing1, OrganizationConnection existing2, ScimConfig config, SutProvider sutProvider) { existing1.Type = existing2.Type = type; existing1.Config = JsonSerializer.Serialize(config); var typedModel = RequestModelFromEntity(existing1); sutProvider.GetDependency().OrganizationOwner(typedModel.OrganizationId).Returns(true); sutProvider.GetDependency() .GetByIdAsync(existing1.Id) .Returns(existing1); sutProvider.GetDependency().ManageScim(typedModel.OrganizationId).Returns(true); sutProvider.GetDependency() .GetByOrganizationIdTypeAsync(typedModel.OrganizationId, type) .Returns(new[] { existing1, existing2 }); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateConnection(existing1.Id, typedModel)); Assert.Contains($"The requested organization already has a connection of type {typedModel.Type}. Only one of each connection type may exist per organization.", exception.Message); } [Theory] [BitAutoData] public async Task UpdateConnection_Success(OrganizationConnection existing, BillingSyncConfig config, OrganizationConnection updated, SutProvider sutProvider) { existing.SetConfig(new BillingSyncConfig { CloudOrganizationId = config.CloudOrganizationId, }); updated.Config = JsonSerializer.Serialize(config); updated.Id = existing.Id; updated.Type = OrganizationConnectionType.CloudBillingSync; var model = RequestModelFromEntity(updated); sutProvider.GetDependency().SelfHosted.Returns(true); sutProvider.GetDependency().OrganizationOwner(model.OrganizationId).Returns(true); sutProvider.GetDependency() .GetByOrganizationIdTypeAsync(model.OrganizationId, model.Type) .Returns(new[] { existing }); sutProvider.GetDependency() .UpdateAsync(default) .ReturnsForAnyArgs(updated); sutProvider.GetDependency() .GetByIdAsync(existing.Id) .Returns(existing); OrganizationLicense organizationLicense = new OrganizationLicense(); var now = DateTime.UtcNow; organizationLicense.Issued = now.AddDays(-10); organizationLicense.Expires = now.AddDays(10); organizationLicense.Version = 1; organizationLicense.UsersGetPremium = true; organizationLicense.Id = config.CloudOrganizationId; organizationLicense.Trial = true; sutProvider.GetDependency() .ReadOrganizationLicenseAsync(Arg.Any()) .Returns(organizationLicense); sutProvider.GetDependency() .VerifyLicense(organizationLicense) .Returns(true); var expected = new OrganizationConnectionResponseModel(updated, typeof(BillingSyncConfig)); var result = await sutProvider.Sut.UpdateConnection(existing.Id, model); AssertHelper.AssertPropertyEqual(expected, result); await sutProvider.GetDependency().Received(1) .UpdateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(model.ToData(updated.Id)))); } [Theory] [BitAutoData] public async Task UpdateConnection_BillingSyncType_InvalidLicense_ErrorThrows(OrganizationConnection existing, BillingSyncConfig config, OrganizationConnection updated, SutProvider sutProvider) { existing.SetConfig(new BillingSyncConfig { CloudOrganizationId = config.CloudOrganizationId, }); updated.Config = JsonSerializer.Serialize(config); updated.Id = existing.Id; updated.Type = OrganizationConnectionType.CloudBillingSync; var model = RequestModelFromEntity(updated); sutProvider.GetDependency().SelfHosted.Returns(true); sutProvider.GetDependency().OrganizationOwner(model.OrganizationId).Returns(true); sutProvider.GetDependency() .GetByOrganizationIdTypeAsync(model.OrganizationId, model.Type) .Returns(new[] { existing }); sutProvider.GetDependency() .UpdateAsync(default) .ReturnsForAnyArgs(updated); sutProvider.GetDependency() .GetByIdAsync(existing.Id) .Returns(existing); OrganizationLicense organizationLicense = new OrganizationLicense(); var now = DateTime.UtcNow; organizationLicense.Issued = now.AddDays(-10); organizationLicense.Expires = now.AddDays(10); organizationLicense.Version = 1; organizationLicense.UsersGetPremium = true; organizationLicense.Id = config.CloudOrganizationId; organizationLicense.Trial = true; sutProvider.GetDependency() .VerifyLicense(organizationLicense) .Returns(false); var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateConnection(existing.Id, model)); Assert.Contains("Cannot verify license file.", exception.Message); } [Theory] [BitAutoData] public async Task UpdateConnection_DoesNotExist_ThrowsNotFound(SutProvider sutProvider) { await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateConnection(Guid.NewGuid(), null)); } [Theory] [BitAutoData] public async Task GetConnection_RequiresOwnerPermissions(Guid connectionId, SutProvider sutProvider) { var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.GetConnection(connectionId, OrganizationConnectionType.CloudBillingSync)); Assert.Contains("You do not have permission to retrieve a connection of type", exception.Message); } [Theory] [BitAutoData] public async Task GetConnection_Success(OrganizationConnection connection, BillingSyncConfig config, SutProvider sutProvider) { connection.Config = JsonSerializer.Serialize(config); sutProvider.GetDependency().SelfHosted.Returns(true); sutProvider.GetDependency() .GetByOrganizationIdTypeAsync(connection.OrganizationId, connection.Type) .Returns(new[] { connection }); sutProvider.GetDependency().OrganizationOwner(connection.OrganizationId).Returns(true); var expected = new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig)); var actual = await sutProvider.Sut.GetConnection(connection.OrganizationId, connection.Type); AssertHelper.AssertPropertyEqual(expected, actual); } [Theory] [BitAutoData] public async Task DeleteConnection_NotFound(Guid connectionId, SutProvider sutProvider) { await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteConnection(connectionId)); } [Theory] [BitAutoData] public async Task DeleteConnection_RequiresOwnerPermissions(OrganizationConnection connection, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(connection.Id).Returns(connection); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteConnection(connection.Id)); Assert.Contains("You do not have permission to remove this connection of type", exception.Message); } [Theory] [BitAutoData] public async Task DeleteConnection_Success(OrganizationConnection connection, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(connection.Id).Returns(connection); sutProvider.GetDependency().OrganizationOwner(connection.OrganizationId).Returns(true); await sutProvider.Sut.DeleteConnection(connection.Id); await sutProvider.GetDependency().DeleteAsync(connection); } private static OrganizationConnectionRequestModel RequestModelFromEntity(OrganizationConnection entity) where T : IConnectionConfig { return new(new OrganizationConnectionRequestModel() { Type = entity.Type, OrganizationId = entity.OrganizationId, Enabled = entity.Enabled, Config = JsonDocument.Parse(entity.Config), }); } private static JsonDocument JsonDocumentFromObject(T obj) => JsonDocument.Parse(JsonSerializer.Serialize(obj)); }