mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05:00
Merge branch 'main' into ac/pm-19145-refactor-OrganizationService.ImportAsync
This commit is contained in:
@ -5,7 +5,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
|
@ -14,7 +14,7 @@ public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOu
|
||||
[InlineData(60000)]
|
||||
public async Task GetAsync(int seats)
|
||||
{
|
||||
await using var factory = new ApiApplicationFactory();
|
||||
await using var factory = new SqlServerApiApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
|
@ -1,10 +1,11 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.IntegrationTestCommon;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Xunit;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -12,16 +13,19 @@ namespace Bit.Api.IntegrationTest.Factories;
|
||||
|
||||
public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
{
|
||||
private readonly IdentityApplicationFactory _identityApplicationFactory;
|
||||
private const string _connectionString = "DataSource=:memory:";
|
||||
protected IdentityApplicationFactory _identityApplicationFactory;
|
||||
|
||||
public ApiApplicationFactory()
|
||||
public ApiApplicationFactory() : this(new SqliteTestDatabase())
|
||||
{
|
||||
SqliteConnection = new SqliteConnection(_connectionString);
|
||||
SqliteConnection.Open();
|
||||
}
|
||||
|
||||
protected ApiApplicationFactory(ITestDatabase db)
|
||||
{
|
||||
TestDatabase = db;
|
||||
|
||||
_identityApplicationFactory = new IdentityApplicationFactory();
|
||||
_identityApplicationFactory.SqliteConnection = SqliteConnection;
|
||||
_identityApplicationFactory.TestDatabase = TestDatabase;
|
||||
_identityApplicationFactory.ManagesDatabase = false;
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
@ -47,6 +51,10 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(
|
||||
string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash")
|
||||
{
|
||||
// This might be the first action in a test and since it forwards to the Identity server, we need to ensure that
|
||||
// this server is initialized since it's responsible for seeding the database.
|
||||
Assert.NotNull(Services);
|
||||
|
||||
await _identityApplicationFactory.RegisterNewIdentityFactoryUserAsync(
|
||||
new RegisterFinishRequestModel
|
||||
{
|
||||
@ -73,12 +81,6 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
SqliteConnection!.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper for logging in via client secret.
|
||||
/// Currently used for Secrets Manager service accounts
|
||||
|
@ -0,0 +1,7 @@
|
||||
using Bit.IntegrationTestCommon;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.IntegrationTest.Factories;
|
||||
|
||||
public class SqlServerApiApplicationFactory() : ApiApplicationFactory(new SqlServerTestDatabase());
|
@ -3,10 +3,10 @@ using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
@ -238,20 +238,13 @@ public class OrganizationUsersControllerTests
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Invite(organizationAbility.Id, model));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true)]
|
||||
[BitAutoData(false)]
|
||||
[Theory, BitAutoData]
|
||||
public async Task Get_ReturnsUser(
|
||||
bool accountDeprovisioningEnabled,
|
||||
OrganizationUserUserDetails organizationUser, ICollection<CollectionAccessSelection> collections,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
organizationUser.Permissions = null;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(accountDeprovisioningEnabled);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManageUsers(organizationUser.OrganizationId)
|
||||
.Returns(true);
|
||||
@ -267,8 +260,8 @@ public class OrganizationUsersControllerTests
|
||||
var response = await sutProvider.Sut.Get(organizationUser.Id, false);
|
||||
|
||||
Assert.Equal(organizationUser.Id, response.Id);
|
||||
Assert.Equal(accountDeprovisioningEnabled, response.ManagedByOrganization);
|
||||
Assert.Equal(accountDeprovisioningEnabled, response.ClaimedByOrganization);
|
||||
Assert.True(response.ManagedByOrganization);
|
||||
Assert.True(response.ClaimedByOrganization);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -21,7 +21,7 @@ using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -140,7 +140,6 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { null });
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
|
||||
|
||||
@ -170,7 +169,6 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { { foundOrg } });
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
|
||||
|
||||
@ -205,7 +203,6 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization>());
|
||||
|
||||
await _sut.Leave(orgId);
|
||||
|
@ -6,7 +6,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
@ -1,7 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;
|
||||
|
@ -7,7 +7,6 @@ using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
@ -193,21 +192,6 @@ public class AccountsControllerTests : IDisposable
|
||||
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldChangeUserEmail()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
_userService.ChangeEmailAsync(user, default, default, default, default, default)
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false);
|
||||
|
||||
await _sut.PostEmail(new EmailRequestModel());
|
||||
|
||||
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
|
||||
{
|
||||
@ -537,12 +521,11 @@ public class AccountsControllerTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserManagedByAnOrganization_ThrowsBadRequestException()
|
||||
public async Task Delete_WithUserManagedByAnOrganization_ThrowsBadRequestException()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(true);
|
||||
|
||||
var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel()));
|
||||
@ -551,12 +534,11 @@ public class AccountsControllerTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserNotManagedByAnOrganization_ShouldSucceed()
|
||||
public async Task Delete_WithUserNotManagedByAnOrganization_ShouldSucceed()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false);
|
||||
_userService.DeleteAsync(user).Returns(IdentityResult.Success);
|
||||
|
||||
|
@ -52,7 +52,7 @@ public class OrganizationBillingControllerTests
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
|
||||
.Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null));
|
||||
.Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null, 0));
|
||||
|
||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||
|
||||
|
@ -23,7 +23,6 @@ using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Services;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
@ -46,7 +45,6 @@ public class OrganizationsControllerTests : IDisposable
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
|
||||
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationInstallationRepository _organizationInstallationRepository;
|
||||
@ -71,7 +69,6 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
|
||||
_upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();
|
||||
_addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>();
|
||||
_referenceEventService = Substitute.For<IReferenceEventService>();
|
||||
_subscriberService = Substitute.For<ISubscriberService>();
|
||||
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
|
||||
_organizationInstallationRepository = Substitute.For<IOrganizationInstallationRepository>();
|
||||
@ -90,7 +87,6 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_updateSecretsManagerSubscriptionCommand,
|
||||
_upgradeOrganizationPlanCommand,
|
||||
_addSecretsManagerSubscriptionCommand,
|
||||
_referenceEventService,
|
||||
_subscriberService,
|
||||
_organizationInstallationRepository,
|
||||
_pricingClient);
|
||||
|
@ -1,16 +1,19 @@
|
||||
using Bit.Api.Billing.Controllers;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
@ -285,6 +288,19 @@ public class ProviderBillingControllerTests
|
||||
Discount = new Discount { Coupon = new Coupon { PercentOff = 10 } },
|
||||
TaxIds = new StripeList<TaxId> { Data = [new TaxId { Value = "123456789" }] }
|
||||
},
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = [
|
||||
new SubscriptionItem
|
||||
{
|
||||
Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }
|
||||
},
|
||||
new SubscriptionItem
|
||||
{
|
||||
Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams }
|
||||
}
|
||||
]
|
||||
},
|
||||
Status = "unpaid",
|
||||
};
|
||||
|
||||
@ -330,11 +346,21 @@ public class ProviderBillingControllerTests
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
foreach (var providerPlan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(StaticStore.GetPlan(providerPlan.PlanType));
|
||||
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);
|
||||
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);
|
||||
sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId)
|
||||
.Returns(new Price
|
||||
{
|
||||
UnitAmountDecimal = plan.PasswordManager.ProviderPortalSeatPrice * 100
|
||||
});
|
||||
}
|
||||
|
||||
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
|
||||
|
@ -23,11 +23,11 @@ public class SendRotationValidatorTests
|
||||
public async Task ValidateAsync_Success()
|
||||
{
|
||||
// Arrange
|
||||
var sendService = Substitute.For<ISendService>();
|
||||
var sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
|
||||
var sendRepository = Substitute.For<ISendRepository>();
|
||||
|
||||
var sut = new SendRotationValidator(
|
||||
sendService,
|
||||
sendAuthorizationService,
|
||||
sendRepository
|
||||
);
|
||||
|
||||
@ -52,11 +52,11 @@ public class SendRotationValidatorTests
|
||||
public async Task ValidateAsync_SendNotReturnedFromRepository_NotIncludedInOutput()
|
||||
{
|
||||
// Arrange
|
||||
var sendService = Substitute.For<ISendService>();
|
||||
var sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
|
||||
var sendRepository = Substitute.For<ISendRepository>();
|
||||
|
||||
var sut = new SendRotationValidator(
|
||||
sendService,
|
||||
sendAuthorizationService,
|
||||
sendRepository
|
||||
);
|
||||
|
||||
@ -76,11 +76,11 @@ public class SendRotationValidatorTests
|
||||
public async Task ValidateAsync_InputMissingUserSend_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var sendService = Substitute.For<ISendService>();
|
||||
var sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
|
||||
var sendRepository = Substitute.For<ISendRepository>();
|
||||
|
||||
var sut = new SendRotationValidator(
|
||||
sendService,
|
||||
sendAuthorizationService,
|
||||
sendRepository
|
||||
);
|
||||
|
||||
|
@ -3,14 +3,15 @@ using AutoFixture.Xunit2;
|
||||
using Bit.Api.Tools.Controllers;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -26,29 +27,32 @@ public class SendsControllerTests : IDisposable
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly ISendService _sendService;
|
||||
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
|
||||
private readonly IAnonymousSendCommand _anonymousSendCommand;
|
||||
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||
private readonly ISendFileStorageService _sendFileStorageService;
|
||||
private readonly ILogger<SendsController> _logger;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public SendsControllerTests()
|
||||
{
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_sendRepository = Substitute.For<ISendRepository>();
|
||||
_sendService = Substitute.For<ISendService>();
|
||||
_nonAnonymousSendCommand = Substitute.For<INonAnonymousSendCommand>();
|
||||
_anonymousSendCommand = Substitute.For<IAnonymousSendCommand>();
|
||||
_sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
|
||||
_sendFileStorageService = Substitute.For<ISendFileStorageService>();
|
||||
_globalSettings = new GlobalSettings();
|
||||
_logger = Substitute.For<ILogger<SendsController>>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
|
||||
_sut = new SendsController(
|
||||
_sendRepository,
|
||||
_userService,
|
||||
_sendService,
|
||||
_sendAuthorizationService,
|
||||
_anonymousSendCommand,
|
||||
_nonAnonymousSendCommand,
|
||||
_sendFileStorageService,
|
||||
_logger,
|
||||
_globalSettings,
|
||||
_currentContext
|
||||
_globalSettings
|
||||
);
|
||||
}
|
||||
|
||||
@ -68,7 +72,8 @@ public class SendsControllerTests : IDisposable
|
||||
send.Data = JsonSerializer.Serialize(new Dictionary<string, string>());
|
||||
send.HideEmail = true;
|
||||
|
||||
_sendService.AccessAsync(id, null).Returns((send, false, false));
|
||||
_sendRepository.GetByIdAsync(Arg.Any<Guid>()).Returns(send);
|
||||
_sendAuthorizationService.AccessAsync(send, null).Returns(SendAccessResult.Granted);
|
||||
_userService.GetUserByIdAsync(Arg.Any<Guid>()).Returns(user);
|
||||
|
||||
var request = new SendAccessRequestModel();
|
||||
|
@ -34,11 +34,11 @@ public class SendRequestModelTests
|
||||
Type = SendType.Text,
|
||||
};
|
||||
|
||||
var sendService = Substitute.For<ISendService>();
|
||||
sendService.HashPassword(Arg.Any<string>())
|
||||
var sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
|
||||
sendAuthorizationService.HashPassword(Arg.Any<string>())
|
||||
.Returns((info) => $"hashed_{(string)info[0]}");
|
||||
|
||||
var send = sendRequest.ToSend(Guid.NewGuid(), sendService);
|
||||
var send = sendRequest.ToSend(Guid.NewGuid(), sendAuthorizationService);
|
||||
|
||||
Assert.Equal(deletionDate, send.DeletionDate);
|
||||
Assert.False(send.Disabled);
|
||||
|
@ -193,49 +193,6 @@ public class CiphersControllerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(false)]
|
||||
[BitAutoData(false)]
|
||||
[BitAutoData(true)]
|
||||
public async Task CanEditCiphersAsAdminAsync_Providers(
|
||||
bool restrictProviders, CipherDetails cipherDetails, CurrentContextOrganization organization, Guid userId, SutProvider<CiphersController> sutProvider
|
||||
)
|
||||
{
|
||||
cipherDetails.OrganizationId = organization.Id;
|
||||
|
||||
// Simulate that the user is a provider for the organization
|
||||
sutProvider.GetDependency<ICurrentContext>().EditAnyCollection(organization.Id).Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(organization.Id).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails });
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
|
||||
{
|
||||
Id = organization.Id,
|
||||
AllowAdminAccessToAllCollectionItems = false
|
||||
});
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(restrictProviders);
|
||||
|
||||
// Non restricted providers should succeed
|
||||
if (!restrictProviders)
|
||||
{
|
||||
await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
|
||||
await sutProvider.GetDependency<ICipherService>().ReceivedWithAnyArgs()
|
||||
.DeleteAsync(default, default);
|
||||
}
|
||||
else // Otherwise, they should fail
|
||||
{
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipherDetails.Id));
|
||||
await sutProvider.GetDependency<ICipherService>().DidNotReceiveWithAnyArgs()
|
||||
.DeleteAsync(default, default);
|
||||
}
|
||||
|
||||
await sutProvider.GetDependency<ICurrentContext>().Received().ProviderUserForOrgAsync(organization.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
@ -456,24 +413,7 @@ public class CiphersControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAdmin_WithProviderUser_DeletesCipher(
|
||||
CipherDetails cipherDetails, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
cipherDetails.OrganizationId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(cipherDetails.OrganizationId.Value).Returns(new List<Cipher> { cipherDetails });
|
||||
|
||||
await sutProvider.Sut.DeleteAdmin(cipherDetails.Id);
|
||||
|
||||
await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(cipherDetails, userId, true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException(
|
||||
public async Task DeleteAdmin_WithProviderUser_ThrowsNotFoundException(
|
||||
Cipher cipher, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
cipher.OrganizationId = Guid.NewGuid();
|
||||
@ -481,7 +421,6 @@ public class CiphersControllerTests
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id));
|
||||
}
|
||||
@ -737,43 +676,13 @@ public class CiphersControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyAdmin_WithProviderUser_DeletesCiphers(
|
||||
CipherBulkDeleteRequestModel model, Guid userId,
|
||||
List<Cipher> ciphers, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
model.OrganizationId = organizationId.ToString();
|
||||
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
|
||||
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
cipher.OrganizationId = organizationId;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organizationId).Returns(ciphers);
|
||||
|
||||
await sutProvider.Sut.DeleteManyAdmin(model);
|
||||
|
||||
await sutProvider.GetDependency<ICipherService>()
|
||||
.Received(1)
|
||||
.DeleteManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids =>
|
||||
ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),
|
||||
userId, organizationId, true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException(
|
||||
public async Task DeleteManyAdmin_WithProviderUser_ThrowsNotFoundException(
|
||||
CipherBulkDeleteRequestModel model, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
model.OrganizationId = organizationId.ToString();
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteManyAdmin(model));
|
||||
}
|
||||
@ -1000,24 +909,7 @@ public class CiphersControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutDeleteAdmin_WithProviderUser_SoftDeletesCipher(
|
||||
CipherDetails cipherDetails, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
cipherDetails.OrganizationId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(cipherDetails.OrganizationId.Value).Returns(new List<Cipher> { cipherDetails });
|
||||
|
||||
await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
|
||||
|
||||
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutDeleteAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException(
|
||||
public async Task PutDeleteAdmin_WithProviderUser_ThrowsNotFoundException(
|
||||
Cipher cipher, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
cipher.OrganizationId = Guid.NewGuid();
|
||||
@ -1025,7 +917,6 @@ public class CiphersControllerTests
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteAdmin(cipher.Id));
|
||||
}
|
||||
@ -1272,43 +1163,13 @@ public class CiphersControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutDeleteManyAdmin_WithProviderUser_SoftDeletesCiphers(
|
||||
CipherBulkDeleteRequestModel model, Guid userId,
|
||||
List<Cipher> ciphers, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
model.OrganizationId = organizationId.ToString();
|
||||
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
|
||||
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
cipher.OrganizationId = organizationId;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organizationId).Returns(ciphers);
|
||||
|
||||
await sutProvider.Sut.PutDeleteManyAdmin(model);
|
||||
|
||||
await sutProvider.GetDependency<ICipherService>()
|
||||
.Received(1)
|
||||
.SoftDeleteManyAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids =>
|
||||
ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),
|
||||
userId, organizationId, true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutDeleteManyAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException(
|
||||
public async Task PutDeleteManyAdmin_WithProviderUser_ThrowsNotFoundException(
|
||||
CipherBulkDeleteRequestModel model, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
model.OrganizationId = organizationId.ToString();
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteManyAdmin(model));
|
||||
}
|
||||
@ -1546,27 +1407,7 @@ public class CiphersControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutRestoreAdmin_WithProviderUser_RestoresCipher(
|
||||
CipherDetails cipherDetails, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
cipherDetails.OrganizationId = Guid.NewGuid();
|
||||
cipherDetails.Type = CipherType.Login;
|
||||
cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(cipherDetails.OrganizationId.Value).Returns(new List<Cipher> { cipherDetails });
|
||||
|
||||
var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
|
||||
|
||||
Assert.IsType<CipherMiniResponseModel>(result);
|
||||
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(cipherDetails, userId, true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutRestoreAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException(
|
||||
public async Task PutRestoreAdmin_WithProviderUser_ThrowsNotFoundException(
|
||||
CipherDetails cipherDetails, Guid userId, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
cipherDetails.OrganizationId = Guid.NewGuid();
|
||||
@ -1574,7 +1415,6 @@ public class CiphersControllerTests
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));
|
||||
}
|
||||
@ -1896,50 +1736,185 @@ public class CiphersControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutRestoreManyAdmin_WithProviderUser_RestoresCiphers(
|
||||
CipherBulkRestoreRequestModel model, Guid userId,
|
||||
List<Cipher> ciphers, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
model.OrganizationId = Guid.NewGuid();
|
||||
model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(model.OrganizationId).Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(model.OrganizationId).Returns(ciphers);
|
||||
|
||||
var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails
|
||||
{
|
||||
Id = c.Id,
|
||||
OrganizationId = model.OrganizationId
|
||||
}).ToList();
|
||||
|
||||
sutProvider.GetDependency<ICipherService>()
|
||||
.RestoreManyAsync(
|
||||
Arg.Any<HashSet<Guid>>(),
|
||||
userId, model.OrganizationId, true)
|
||||
.Returns(cipherOrgDetails);
|
||||
|
||||
var result = await sutProvider.Sut.PutRestoreManyAdmin(model);
|
||||
|
||||
Assert.NotNull(result);
|
||||
await sutProvider.GetDependency<ICipherService>()
|
||||
.Received(1)
|
||||
.RestoreManyAsync(
|
||||
Arg.Is<HashSet<Guid>>(ids =>
|
||||
ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),
|
||||
userId, model.OrganizationId, true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutRestoreManyAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException(
|
||||
public async Task PutRestoreManyAdmin_WithProviderUser_ThrowsNotFoundException(
|
||||
CipherBulkRestoreRequestModel model, SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
model.OrganizationId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(model.OrganizationId).Returns(true);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ProviderUserForOrgAsync(new Guid(model.OrganizationId.ToString()))
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreManyAdmin(model));
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => sutProvider.Sut.PutRestoreManyAdmin(model)
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutShareMany_ShouldShareCiphersAndReturnRevisionDateMap(
|
||||
User user,
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var oldDate1 = DateTime.UtcNow.AddDays(-1);
|
||||
var oldDate2 = DateTime.UtcNow.AddDays(-2);
|
||||
var detail1 = new CipherDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
OrganizationId = organizationId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new CipherLoginData()),
|
||||
RevisionDate = oldDate1
|
||||
};
|
||||
var detail2 = new CipherDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
OrganizationId = organizationId,
|
||||
Type = CipherType.SecureNote,
|
||||
Data = JsonSerializer.Serialize(new CipherSecureNoteData()),
|
||||
RevisionDate = oldDate2
|
||||
};
|
||||
var preloadedDetails = new List<CipherDetails> { detail1, detail2 };
|
||||
|
||||
var newDate1 = oldDate1.AddMinutes(5);
|
||||
var newDate2 = oldDate2.AddMinutes(5);
|
||||
var updatedCipher1 = new Cipher { Id = detail1.Id, RevisionDate = newDate1, Type = detail1.Type, Data = detail1.Data };
|
||||
var updatedCipher2 = new Cipher { Id = detail2.Id, RevisionDate = newDate2, Type = detail2.Type, Data = detail2.Data };
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(Task.FromResult(true));
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(Task.FromResult(user));
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(default!)
|
||||
.ReturnsForAnyArgs(userId);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(userId, withOrganizations: false)
|
||||
.Returns(Task.FromResult((ICollection<CipherDetails>)preloadedDetails));
|
||||
|
||||
sutProvider.GetDependency<ICipherService>()
|
||||
.ShareManyAsync(
|
||||
Arg.Any<IEnumerable<(Cipher, DateTime?)>>(),
|
||||
organizationId,
|
||||
Arg.Any<IEnumerable<Guid>>(),
|
||||
userId
|
||||
)
|
||||
.Returns(Task.FromResult<IEnumerable<Cipher>>(new[] { updatedCipher1, updatedCipher2 }));
|
||||
|
||||
var cipherRequests = preloadedDetails.Select(d => new CipherWithIdRequestModel
|
||||
{
|
||||
Id = d.Id,
|
||||
OrganizationId = d.OrganizationId!.Value.ToString(),
|
||||
LastKnownRevisionDate = d.RevisionDate,
|
||||
Type = d.Type
|
||||
}).ToList();
|
||||
|
||||
var model = new CipherBulkShareRequestModel
|
||||
{
|
||||
Ciphers = cipherRequests,
|
||||
CollectionIds = new[] { Guid.NewGuid().ToString() }
|
||||
};
|
||||
|
||||
var result = await sutProvider.Sut.PutShareMany(model);
|
||||
|
||||
Assert.Equal(2, result.Length);
|
||||
var revisionDates = result.Select(r => r.RevisionDate).ToList();
|
||||
Assert.Contains(newDate1, revisionDates);
|
||||
Assert.Contains(newDate2, revisionDates);
|
||||
|
||||
await sutProvider.GetDependency<ICipherService>()
|
||||
.Received(1)
|
||||
.ShareManyAsync(
|
||||
Arg.Is<IEnumerable<(Cipher, DateTime?)>>(list =>
|
||||
list.Select(x => x.Item1.Id).OrderBy(id => id)
|
||||
.SequenceEqual(new[] { detail1.Id, detail2.Id }.OrderBy(id => id))
|
||||
),
|
||||
organizationId,
|
||||
Arg.Any<IEnumerable<Guid>>(),
|
||||
userId
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShareMany_OrganizationUserFalse_ThrowsNotFound(
|
||||
CipherBulkShareRequestModel model,
|
||||
SutProvider<CiphersController> sut)
|
||||
{
|
||||
model.Ciphers = new[] {
|
||||
new CipherWithIdRequestModel { Id = Guid.NewGuid(), OrganizationId = Guid.NewGuid().ToString() }
|
||||
};
|
||||
sut.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(Arg.Any<Guid>())
|
||||
.Returns(Task.FromResult(false));
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sut.Sut.PutShareMany(model));
|
||||
}
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShareMany_CipherNotOwned_ThrowsNotFoundException(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
CipherWithIdRequestModel request,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
request.EncryptedFor = userId;
|
||||
var model = new CipherBulkShareRequestModel
|
||||
{
|
||||
Ciphers = new[] { request },
|
||||
CollectionIds = new[] { Guid.NewGuid().ToString() }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(Task.FromResult(true));
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(default)
|
||||
.ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(userId, withOrganizations: false)
|
||||
.Returns(Task.FromResult((ICollection<CipherDetails>)new List<CipherDetails>()));
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => sutProvider.Sut.PutShareMany(model)
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShareMany_EncryptedForWrongUser_ThrowsNotFoundException(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
CipherWithIdRequestModel request,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
request.EncryptedFor = Guid.NewGuid(); // not equal to userId
|
||||
var model = new CipherBulkShareRequestModel
|
||||
{
|
||||
Ciphers = new[] { request },
|
||||
CollectionIds = new[] { Guid.NewGuid().ToString() }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(Task.FromResult(true));
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(default)
|
||||
.ReturnsForAnyArgs(userId);
|
||||
|
||||
var existing = new CipherDetails { Id = request.Id.Value };
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(userId, withOrganizations: false)
|
||||
.Returns(Task.FromResult((ICollection<CipherDetails>)(new[] { existing })));
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => sutProvider.Sut.PutShareMany(model)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReceivedExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Billing.Test.Controllers;
|
||||
@ -71,6 +72,41 @@ public class FreshdeskControllerTests
|
||||
_ = mockHttpMessageHandler.Received(1).Send(Arg.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Post && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes")), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhook_add_note_when_user_is_invalid(
|
||||
string freshdeskWebhookKey, FreshdeskWebhookModel model,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
// Arrange - for an invalid user
|
||||
model.TicketContactEmail = "invalid@user";
|
||||
sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(model.TicketContactEmail).Returns((User)null);
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>().Value.FreshDesk.WebhookKey.Returns(WebhookKey);
|
||||
|
||||
var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
|
||||
mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockResponse);
|
||||
var httpClient = new HttpClient(mockHttpMessageHandler);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(httpClient);
|
||||
|
||||
// Act
|
||||
var response = await sutProvider.Sut.PostWebhook(freshdeskWebhookKey, model);
|
||||
|
||||
// Assert
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode);
|
||||
|
||||
await mockHttpMessageHandler
|
||||
.Received(1).Send(
|
||||
Arg.Is<HttpRequestMessage>(
|
||||
m => m.Method == HttpMethod.Post
|
||||
&& m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes")
|
||||
&& m.Content.ReadAsStringAsync().Result.Contains("No user found")),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[BitAutoData((string)null, null)]
|
||||
[BitAutoData((string)null)]
|
||||
|
@ -4,10 +4,10 @@ using Bit.Billing.Test.Utilities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
|
@ -27,9 +27,9 @@ public class SutProvider<TSut> : ISutProvider
|
||||
=> SetDependency(typeof(T), dependency, parameterName);
|
||||
public SutProvider<TSut> SetDependency(Type dependencyType, object dependency, string parameterName = "")
|
||||
{
|
||||
if (_dependencies.ContainsKey(dependencyType))
|
||||
if (_dependencies.TryGetValue(dependencyType, out var dependencyForType))
|
||||
{
|
||||
_dependencies[dependencyType][parameterName] = dependency;
|
||||
dependencyForType[parameterName] = dependency;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -46,12 +46,11 @@ public class SutProvider<TSut> : ISutProvider
|
||||
{
|
||||
return _dependencies[dependencyType][parameterName];
|
||||
}
|
||||
else if (_dependencies.ContainsKey(dependencyType))
|
||||
else if (_dependencies.TryGetValue(dependencyType, out var knownDependencies))
|
||||
{
|
||||
var knownDependencies = _dependencies[dependencyType];
|
||||
if (knownDependencies.Values.Count == 1)
|
||||
{
|
||||
return _dependencies[dependencyType].Values.Single();
|
||||
return knownDependencies.Values.Single();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -0,0 +1,54 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
using Bit.Core.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Data.Integrations;
|
||||
|
||||
public class IntegrationMessageTests
|
||||
{
|
||||
[Fact]
|
||||
public void ApplyRetry_IncrementsRetryCountAndSetsDelayUntilDate()
|
||||
{
|
||||
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
RetryCount = 2,
|
||||
DelayUntilDate = null
|
||||
};
|
||||
|
||||
var baseline = DateTime.UtcNow;
|
||||
message.ApplyRetry(baseline);
|
||||
|
||||
Assert.Equal(3, message.RetryCount);
|
||||
Assert.NotNull(message.DelayUntilDate);
|
||||
Assert.True(message.DelayUntilDate > baseline);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromToJson_SerializesCorrectly()
|
||||
{
|
||||
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
|
||||
RenderedTemplate = "This is the message",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RetryCount = 2,
|
||||
DelayUntilDate = null
|
||||
};
|
||||
|
||||
var json = message.ToJson();
|
||||
var result = IntegrationMessage<WebhookIntegrationConfigurationDetails>.FromJson(json);
|
||||
|
||||
Assert.Equal(message.Configuration, result.Configuration);
|
||||
Assert.Equal(message.RenderedTemplate, result.RenderedTemplate);
|
||||
Assert.Equal(message.IntegrationType, result.IntegrationType);
|
||||
Assert.Equal(message.RetryCount, result.RetryCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_InvalidJson_ThrowsJsonException()
|
||||
{
|
||||
var json = "{ Invalid JSON";
|
||||
Assert.Throws<JsonException>(() => IntegrationMessage<WebhookIntegrationConfigurationDetails>.FromJson(json));
|
||||
}
|
||||
}
|
@ -6,9 +6,6 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@ -27,7 +24,6 @@ public class CreateGroupCommandTests
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).CreateAsync(group);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Created);
|
||||
await sutProvider.GetDependency<IReferenceEventService>().Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(r => r.Type == ReferenceEventType.GroupCreated && r.Id == organization.Id && r.Source == ReferenceEventSource.Organization));
|
||||
AssertHelper.AssertRecent(group.CreationDate);
|
||||
AssertHelper.AssertRecent(group.RevisionDate);
|
||||
}
|
||||
@ -48,7 +44,6 @@ public class CreateGroupCommandTests
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).CreateAsync(group, collections);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Created);
|
||||
await sutProvider.GetDependency<IReferenceEventService>().Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(r => r.Type == ReferenceEventType.GroupCreated && r.Id == organization.Id && r.Source == ReferenceEventSource.Organization));
|
||||
AssertHelper.AssertRecent(group.CreationDate);
|
||||
AssertHelper.AssertRecent(group.RevisionDate);
|
||||
}
|
||||
@ -60,7 +55,6 @@ public class CreateGroupCommandTests
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).CreateAsync(group);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Created, eventSystemUser);
|
||||
await sutProvider.GetDependency<IReferenceEventService>().Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(r => r.Type == ReferenceEventType.GroupCreated && r.Id == organization.Id && r.Source == ReferenceEventSource.Organization));
|
||||
AssertHelper.AssertRecent(group.CreationDate);
|
||||
AssertHelper.AssertRecent(group.RevisionDate);
|
||||
}
|
||||
@ -74,7 +68,6 @@ public class CreateGroupCommandTests
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogGroupEventAsync(default, default, default);
|
||||
await sutProvider.GetDependency<IReferenceEventService>().DidNotReceiveWithAnyArgs().RaiseEventAsync(default);
|
||||
}
|
||||
|
||||
[Theory, OrganizationCustomize(UseGroups = false), BitAutoData]
|
||||
@ -86,6 +79,5 @@ public class CreateGroupCommandTests
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogGroupEventAsync(default, default, default);
|
||||
await sutProvider.GetDependency<IReferenceEventService>().DidNotReceiveWithAnyArgs().RaiseEventAsync(default);
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled(
|
||||
public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled(
|
||||
OrganizationDomain domain, Guid userId, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
@ -177,10 +177,6 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.ResolveAsync(domain.DomainName, domain.Txt)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(userId);
|
||||
|
||||
@ -196,33 +192,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled(
|
||||
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IDnsResolverService>()
|
||||
.ResolveAsync(domain.DomainName, domain.Txt)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(Guid.NewGuid());
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(false);
|
||||
|
||||
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
|
||||
|
||||
await sutProvider.GetDependency<ISavePolicyCommand>()
|
||||
.DidNotReceive()
|
||||
.SaveAsync(Arg.Any<PolicyUpdate>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled(
|
||||
public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled(
|
||||
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
@ -236,10 +206,6 @@ public class VerifyOrganizationDomainCommandTests
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(Guid.NewGuid());
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
|
||||
|
||||
await sutProvider.GetDependency<ISavePolicyCommand>()
|
||||
@ -248,33 +214,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled(
|
||||
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IDnsResolverService>()
|
||||
.ResolveAsync(domain.DomainName, domain.Txt)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(Guid.NewGuid());
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
|
||||
|
||||
await sutProvider.GetDependency<ISavePolicyCommand>()
|
||||
.DidNotReceive()
|
||||
.SaveAsync(Arg.Any<PolicyUpdate>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenEmailShouldBeSentToUsersWhoBelongToTheDomain(
|
||||
public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsVerified_ThenEmailShouldBeSentToUsersWhoBelongToTheDomain(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers,
|
||||
OrganizationDomain domain,
|
||||
Organization organization,
|
||||
@ -306,10 +246,6 @@ public class VerifyOrganizationDomainCommandTests
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId.Returns(Guid.NewGuid());
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(domain.OrganizationId)
|
||||
.Returns(mockedUsers);
|
||||
|
@ -1,5 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
@ -29,7 +32,6 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
|
||||
public class AcceptOrgUserCommandTests
|
||||
{
|
||||
private readonly IUserService _userService = Substitute.For<IUserService>();
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
||||
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For<IOrgUserInviteTokenableFactory>();
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
|
||||
|
||||
@ -166,9 +168,6 @@ public class AcceptOrgUserCommandTests
|
||||
// Arrange
|
||||
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
|
||||
|
||||
// User doesn't have 2FA enabled
|
||||
_twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
|
||||
|
||||
// Organization they are trying to join requires 2FA
|
||||
var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId };
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
@ -185,6 +184,107 @@ public class AcceptOrgUserCommandTests
|
||||
exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest(
|
||||
SutProvider<AcceptOrgUserCommand> sutProvider,
|
||||
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
|
||||
{
|
||||
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
// Organization they are trying to join requires 2FA
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = orgUser.OrganizationId,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
}
|
||||
]));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));
|
||||
|
||||
Assert.Equal("You cannot join this organization until you enable two-step login on your user account.",
|
||||
exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWith2FAJoining2FARequiredOrg_Succeeds(
|
||||
SutProvider<AcceptOrgUserCommand> sutProvider,
|
||||
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
|
||||
{
|
||||
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
// User has 2FA enabled
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(user)
|
||||
.Returns(true);
|
||||
|
||||
// Organization they are trying to join requires 2FA
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = orgUser.OrganizationId,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
}
|
||||
]));
|
||||
|
||||
await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<OrganizationUser>(ou => ou.Status == OrganizationUserStatusType.Accepted));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserJoiningOrgWithout2FARequirement_Succeeds(
|
||||
SutProvider<AcceptOrgUserCommand> sutProvider,
|
||||
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
|
||||
{
|
||||
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
// Organization they are trying to join doesn't require 2FA
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
}
|
||||
]));
|
||||
|
||||
await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<OrganizationUser>(ou => ou.Status == OrganizationUserStatusType.Accepted));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
@ -647,9 +747,6 @@ public class AcceptOrgUserCommandTests
|
||||
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
|
||||
.Returns(false);
|
||||
|
||||
// User doesn't have 2FA enabled
|
||||
_twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
|
||||
|
||||
// Org does not require 2FA
|
||||
sutProvider.GetDependency<IPolicyService>().GetPoliciesApplicableToUserAsync(user.Id,
|
||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited)
|
||||
|
@ -1,6 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
@ -321,4 +324,122 @@ public class ConfirmOrganizationUserCommandTests
|
||||
Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
|
||||
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorRequired_ThrowsBadRequestException(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
}
|
||||
]));
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id));
|
||||
Assert.Contains("User does not have two-step login enabled.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorNotRequired_Succeeds(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
}
|
||||
]));
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
||||
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorEnabled_Succeeds(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
organizationRepository.GetByIdAsync(org.Id).Returns(org);
|
||||
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
}
|
||||
]));
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) });
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
||||
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
||||
}
|
||||
}
|
||||
|
@ -25,13 +25,13 @@ public class GetOrganizationUsersClaimedStatusQueryTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetUsersOrganizationManagementStatusAsync_WithUseSsoEnabled_Success(
|
||||
public async Task GetUsersOrganizationManagementStatusAsync_WithUseOrganizationDomainsEnabled_Success(
|
||||
Organization organization,
|
||||
ICollection<OrganizationUser> usersWithClaimedDomain,
|
||||
SutProvider<GetOrganizationUsersClaimedStatusQuery> sutProvider)
|
||||
{
|
||||
organization.Enabled = true;
|
||||
organization.UseSso = true;
|
||||
organization.UseOrganizationDomains = true;
|
||||
|
||||
var userIdWithoutClaimedDomain = Guid.NewGuid();
|
||||
var userIdsToCheck = usersWithClaimedDomain.Select(u => u.Id).Concat(new List<Guid> { userIdWithoutClaimedDomain }).ToList();
|
||||
@ -51,13 +51,13 @@ public class GetOrganizationUsersClaimedStatusQueryTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetUsersOrganizationManagementStatusAsync_WithUseSsoDisabled_ReturnsAllFalse(
|
||||
public async Task GetUsersOrganizationManagementStatusAsync_WithUseOrganizationDomainsDisabled_ReturnsAllFalse(
|
||||
Organization organization,
|
||||
ICollection<OrganizationUser> usersWithClaimedDomain,
|
||||
SutProvider<GetOrganizationUsersClaimedStatusQuery> sutProvider)
|
||||
{
|
||||
organization.Enabled = true;
|
||||
organization.UseSso = false;
|
||||
organization.UseOrganizationDomains = false;
|
||||
|
||||
var userIdWithoutClaimedDomain = Guid.NewGuid();
|
||||
var userIdsToCheck = usersWithClaimedDomain.Select(u => u.Id).Concat(new List<Guid> { userIdWithoutClaimedDomain }).ToList();
|
||||
|
@ -40,43 +40,6 @@ public class RemoveOrganizationUserCommandTests
|
||||
// Act
|
||||
await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetUsersOrganizationClaimedStatusAsync(default, default);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.DeleteAsync(organizationUser);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveUser_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success(
|
||||
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
|
||||
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser,
|
||||
SutProvider<RemoveOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.OrganizationId = deletingUser.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(deletingUser.Id)
|
||||
.Returns(deletingUser);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(deletingUser.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.Received(1)
|
||||
@ -235,15 +198,12 @@ public class RemoveOrganizationUserCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveUserAsync_WithDeletingUserId_WithAccountDeprovisioningEnabled_WhenUserIsManaged_ThrowsException(
|
||||
public async Task RemoveUserAsync_WithDeletingUserId_WhenUserIsManaged_ThrowsException(
|
||||
[OrganizationUser(status: OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser,
|
||||
Guid deletingUserId,
|
||||
SutProvider<RemoveOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(orgUser.Id)
|
||||
.Returns(orgUser);
|
||||
@ -285,34 +245,6 @@ public class RemoveOrganizationUserCommandTests
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveUser_WithEventSystemUser_WithAccountDeprovisioningEnabled_Success(
|
||||
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
|
||||
EventSystemUser eventSystemUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetUsersOrganizationClaimedStatusAsync(default, default);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.DeleteAsync(organizationUser);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RemoveUser_WithEventSystemUser_NotFound_ThrowsException(
|
||||
@ -474,7 +406,6 @@ public class RemoveOrganizationUserCommandTests
|
||||
var sutProvider = SutProviderFactory();
|
||||
var eventDate = sutProvider.GetDependency<FakeTimeProvider>().GetUtcNow().UtcDateTime;
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
|
||||
|
||||
var organizationUsers = new[] { orgUser1, orgUser2 };
|
||||
var organizationUserIds = organizationUsers.Select(u => u.Id);
|
||||
|
||||
@ -499,60 +430,6 @@ public class RemoveOrganizationUserCommandTests
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count());
|
||||
Assert.All(result, r => Assert.Empty(r.ErrorMessage));
|
||||
await sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetUsersOrganizationClaimedStatusAsync(default, default);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)));
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventsAsync(
|
||||
Arg.Is<IEnumerable<(OrganizationUser OrganizationUser, EventType EventType, DateTime? DateTime)>>(i =>
|
||||
i.First().OrganizationUser.Id == orgUser1.Id
|
||||
&& i.Last().OrganizationUser.Id == orgUser2.Id
|
||||
&& i.All(u => u.DateTime == eventDate)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveUsers_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success(
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser,
|
||||
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory();
|
||||
var eventDate = sutProvider.GetDependency<FakeTimeProvider>().GetUtcNow().UtcDateTime;
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
|
||||
var organizationUsers = new[] { orgUser1, orgUser2 };
|
||||
var organizationUserIds = organizationUsers.Select(u => u.Id);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(default)
|
||||
.ReturnsForAnyArgs(organizationUsers);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(deletingUser.Id)
|
||||
.Returns(deletingUser);
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(deletingUser.OrganizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.GetUsersOrganizationClaimedStatusAsync(
|
||||
deletingUser.OrganizationId,
|
||||
Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)))
|
||||
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, false }, { orgUser2.Id, false } });
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count());
|
||||
Assert.All(result, r => Assert.Empty(r.ErrorMessage));
|
||||
@ -638,7 +515,7 @@ public class RemoveOrganizationUserCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveUsers_WithDeletingUserId_RemovingClaimedUser_WithAccountDeprovisioningEnabled_ThrowsException(
|
||||
public async Task RemoveUsers_WithDeletingUserId_RemovingClaimedUser_ThrowsException(
|
||||
[OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser,
|
||||
OrganizationUser deletingUser,
|
||||
SutProvider<RemoveOrganizationUserCommand> sutProvider)
|
||||
@ -646,10 +523,6 @@ public class RemoveOrganizationUserCommandTests
|
||||
// Arrange
|
||||
orgUser.OrganizationId = deletingUser.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser.Id)))
|
||||
.Returns(new[] { orgUser });
|
||||
@ -739,51 +612,6 @@ public class RemoveOrganizationUserCommandTests
|
||||
&& u.DateTime == eventDate)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveUsers_WithEventSystemUser_WithAccountDeprovisioningEnabled_Success(
|
||||
EventSystemUser eventSystemUser,
|
||||
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1,
|
||||
OrganizationUser orgUser2)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory();
|
||||
var eventDate = sutProvider.GetDependency<FakeTimeProvider>().GetUtcNow().UtcDateTime;
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId;
|
||||
var organizationUsers = new[] { orgUser1, orgUser2 };
|
||||
var organizationUserIds = organizationUsers.Select(u => u.Id);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync(default)
|
||||
.ReturnsForAnyArgs(organizationUsers);
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(orgUser1.OrganizationId, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RemoveUsersAsync(orgUser1.OrganizationId, organizationUserIds, eventSystemUser);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count());
|
||||
Assert.All(result, r => Assert.Empty(r.ErrorMessage));
|
||||
await sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetUsersOrganizationClaimedStatusAsync(default, default);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)));
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventsAsync(
|
||||
Arg.Is<IEnumerable<(OrganizationUser OrganizationUser, EventType EventType, EventSystemUser EventSystemUser, DateTime? DateTime)>>(
|
||||
i => i.First().OrganizationUser.Id == orgUser1.Id
|
||||
&& i.Last().OrganizationUser.Id == orgUser2.Id
|
||||
&& i.All(u => u.EventSystemUser == eventSystemUser
|
||||
&& u.DateTime == eventDate)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemoveUsers_WithEventSystemUser_WithMismatchingOrganizationId_ThrowsException(
|
||||
EventSystemUser eventSystemUser,
|
||||
|
@ -1,6 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
@ -208,6 +211,57 @@ public class RestoreOrganizationUserCommandTests
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
organizationUser.Email = null;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) });
|
||||
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Revoked,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
}
|
||||
]));
|
||||
|
||||
var user = new User();
|
||||
user.Email = "test@bitwarden.com";
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
||||
Organization organization,
|
||||
@ -235,6 +289,46 @@ public class RestoreOrganizationUserCommandTests
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Revoked,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
}
|
||||
]));
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails(
|
||||
Organization organization,
|
||||
@ -277,45 +371,6 @@ public class RestoreOrganizationUserCommandTests
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_vNext_WithOtherOrganizationSingleOrgPolicyEnabled_Fails(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns(true);
|
||||
|
||||
var user = new User { Email = "test@bitwarden.com" };
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
|
||||
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails(
|
||||
Organization organization,
|
||||
@ -364,20 +419,42 @@ public class RestoreOrganizationUserCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
|
||||
public async Task RestoreUser_WithPolicyRequirementsEnabled_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
organizationUser.Email = null;
|
||||
|
||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(organizationUser.UserId.Value)
|
||||
.Returns(new[] { organizationUser, secondOrganizationUser });
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
|
||||
]);
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns(new[]
|
||||
{
|
||||
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Revoked,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
}
|
||||
]));
|
||||
|
||||
var user = new User { Email = "test@bitwarden.com" };
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||
@ -385,7 +462,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login policy", exception.Message.ToLowerInvariant());
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
@ -398,35 +475,6 @@ public class RestoreOrganizationUserCommandTests
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WhenUserOwningAnotherFreeOrganization_ThenRestoreUserFails(
|
||||
Organization organization,
|
||||
@ -672,6 +720,77 @@ public class RestoreOrganizationUserCommandTests
|
||||
.RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUsers_WithPolicyRequirementsEnabled_With2FAPolicy_BlocksNonCompliantUser(Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
RestoreUser_Setup(organization, owner, orgUser1, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
|
||||
orgUser1.Email = orgUser2.Email = null;
|
||||
orgUser3.UserId = null;
|
||||
orgUser3.Key = null;
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id;
|
||||
organizationUserRepository
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))
|
||||
.Returns(new[] { orgUser1, orgUser2, orgUser3 });
|
||||
|
||||
userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" });
|
||||
|
||||
// Setup 2FA policy
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(Arg.Any<Guid>())
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Revoked,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
}
|
||||
]));
|
||||
|
||||
// User1 has 2FA, User2 doesn't
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
|
||||
{
|
||||
(orgUser1.UserId!.Value, true),
|
||||
(orgUser2.UserId!.Value, false)
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Empty(result[0].Item2); // First user should succeed
|
||||
Assert.Contains("two-step login", result[1].Item2); // Second user should fail
|
||||
Assert.Empty(result[2].Item2); // Third user should succeed
|
||||
await organizationUserRepository
|
||||
.Received(1)
|
||||
.RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);
|
||||
await organizationUserRepository
|
||||
.DidNotReceive()
|
||||
.RestoreAsync(orgUser2.Id, Arg.Any<OrganizationUserStatusType>());
|
||||
await organizationUserRepository
|
||||
.Received(1)
|
||||
.RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUsers_UserOwnsAnotherFreeOrganization_BlocksOwnerUserFromBeingRestored(Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
|
@ -10,9 +10,6 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -51,15 +48,6 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
|
||||
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
|
||||
referenceEvent.Type == ReferenceEventType.Signup &&
|
||||
referenceEvent.PlanName == plan.Name &&
|
||||
referenceEvent.PlanType == plan.Type &&
|
||||
referenceEvent.Seats == result.Organization.Seats &&
|
||||
referenceEvent.Storage == result.Organization.MaxStorageGb));
|
||||
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
|
||||
|
||||
Assert.NotNull(result.Organization);
|
||||
Assert.NotNull(result.OrganizationUser);
|
||||
|
||||
@ -145,15 +133,6 @@ public class CloudICloudOrganizationSignUpCommandTests
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
|
||||
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
|
||||
referenceEvent.Type == ReferenceEventType.Signup &&
|
||||
referenceEvent.PlanName == plan.Name &&
|
||||
referenceEvent.PlanType == plan.Type &&
|
||||
referenceEvent.Seats == result.Organization.Seats &&
|
||||
referenceEvent.Storage == result.Organization.MaxStorageGb));
|
||||
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
|
||||
|
||||
Assert.NotNull(result.Organization);
|
||||
Assert.NotNull(result.OrganizationUser);
|
||||
|
||||
|
@ -0,0 +1,155 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class ProviderClientOrganizationSignUpCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.TeamsAnnually)]
|
||||
[BitAutoData(PlanType.TeamsMonthly)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||
public async Task SignupClientAsync_ValidParameters_CreatesOrganizationSuccessfully(
|
||||
PlanType planType,
|
||||
OrganizationSignup signup,
|
||||
string collectionName,
|
||||
SutProvider<ProviderClientOrganizationSignUpCommand> sutProvider)
|
||||
{
|
||||
signup.Plan = planType;
|
||||
signup.AdditionalSeats = 15;
|
||||
signup.CollectionName = collectionName;
|
||||
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(signup.Plan)
|
||||
.Returns(plan);
|
||||
|
||||
var result = await sutProvider.Sut.SignUpClientOrganizationAsync(signup);
|
||||
|
||||
Assert.NotNull(result.Organization);
|
||||
Assert.NotNull(result.DefaultCollection);
|
||||
Assert.Equal(collectionName, result.DefaultCollection.Name);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(
|
||||
Arg.Is<Organization>(o =>
|
||||
o.Name == signup.Name &&
|
||||
o.BillingEmail == signup.BillingEmail &&
|
||||
o.PlanType == plan.Type &&
|
||||
o.Seats == signup.AdditionalSeats &&
|
||||
o.MaxCollections == plan.PasswordManager.MaxCollections &&
|
||||
o.UsePasswordManager == true &&
|
||||
o.UseSecretsManager == false &&
|
||||
o.Status == OrganizationStatusType.Created
|
||||
)
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(
|
||||
Arg.Is<Collection>(c =>
|
||||
c.Name == collectionName &&
|
||||
c.OrganizationId == result.Organization.Id
|
||||
),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>()
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationApiKeyRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(
|
||||
Arg.Is<OrganizationApiKey>(k =>
|
||||
k.OrganizationId == result.Organization.Id &&
|
||||
k.Type == OrganizationApiKeyType.Default
|
||||
)
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertOrganizationAbilityAsync(Arg.Is<Organization>(o => o.Id == result.Organization.Id));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SignupClientAsync_NullPlan_ThrowsBadRequestException(
|
||||
OrganizationSignup signup,
|
||||
SutProvider<ProviderClientOrganizationSignUpCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(signup.Plan)
|
||||
.Returns((Plan)null);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SignUpClientOrganizationAsync(signup));
|
||||
|
||||
Assert.Contains(ProviderClientOrganizationSignUpCommand.PlanNullErrorMessage, exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SignupClientAsync_NegativeAdditionalSeats_ThrowsBadRequestException(
|
||||
OrganizationSignup signup,
|
||||
SutProvider<ProviderClientOrganizationSignUpCommand> sutProvider)
|
||||
{
|
||||
signup.Plan = PlanType.TeamsMonthly;
|
||||
signup.AdditionalSeats = -5;
|
||||
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(signup.Plan)
|
||||
.Returns(plan);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SignUpClientOrganizationAsync(signup));
|
||||
|
||||
Assert.Contains(ProviderClientOrganizationSignUpCommand.AdditionalSeatsNegativeErrorMessage, exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.TeamsAnnually)]
|
||||
public async Task SignupClientAsync_WhenExceptionIsThrown_CleanupIsPerformed(
|
||||
PlanType planType,
|
||||
OrganizationSignup signup,
|
||||
SutProvider<ProviderClientOrganizationSignUpCommand> sutProvider)
|
||||
{
|
||||
signup.Plan = planType;
|
||||
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(signup.Plan)
|
||||
.Returns(plan);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationApiKeyRepository>()
|
||||
.When(x => x.CreateAsync(Arg.Any<OrganizationApiKey>()))
|
||||
.Do(_ => throw new Exception());
|
||||
|
||||
var thrownException = await Assert.ThrowsAsync<Exception>(
|
||||
() => sutProvider.Sut.SignUpClientOrganizationAsync(signup));
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.Received(1)
|
||||
.DeleteAsync(Arg.Is<Organization>(o => o.Name == signup.Name));
|
||||
|
||||
await sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteOrganizationAbilityAsync(Arg.Any<Guid>());
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RequireTwoFactorPolicyRequirementFactoryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void IsTwoFactorRequiredForOrganization_WithNoPolicies_ReturnsFalse(
|
||||
Guid organizationId,
|
||||
SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create([]);
|
||||
|
||||
Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void IsTwoFactorRequiredForOrganization_WithOrganizationPolicy_ReturnsTrue(
|
||||
Guid organizationId,
|
||||
SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
}
|
||||
]);
|
||||
|
||||
Assert.True(actual.IsTwoFactorRequiredForOrganization(organizationId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void IsTwoFactorRequiredForOrganization_WithOtherOrganizationPolicy_ReturnsFalse(
|
||||
Guid organizationId,
|
||||
SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
},
|
||||
]);
|
||||
|
||||
Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void OrganizationsRequiringTwoFactor_WithNoPolicies_ReturnsEmptyCollection(
|
||||
SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create([]);
|
||||
|
||||
Assert.Empty(actual.OrganizationsRequiringTwoFactor);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void OrganizationsRequiringTwoFactor_WithMultiplePolicies_ReturnsActiveMemberships(
|
||||
Guid orgId1, Guid orgUserId1, Guid orgId2, Guid orgUserId2,
|
||||
Guid orgId3, Guid orgUserId3, Guid orgId4, Guid orgUserId4,
|
||||
SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var policies = new[]
|
||||
{
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = orgId1,
|
||||
OrganizationUserId = orgUserId1,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Accepted
|
||||
},
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = orgId2,
|
||||
OrganizationUserId = orgUserId2,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed
|
||||
},
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = orgId3,
|
||||
OrganizationUserId = orgUserId3,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited
|
||||
},
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = orgId4,
|
||||
OrganizationUserId = orgUserId4,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Revoked
|
||||
}
|
||||
};
|
||||
|
||||
var actual = sutProvider.Sut.Create(policies);
|
||||
|
||||
var result = actual.OrganizationsRequiringTwoFactor.ToList();
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, p => p.OrganizationId == orgId1 && p.OrganizationUserId == orgUserId1);
|
||||
Assert.Contains(result, p => p.OrganizationId == orgId2 && p.OrganizationUserId == orgUserId2);
|
||||
Assert.DoesNotContain(result, p => p.OrganizationId == orgId3 && p.OrganizationUserId == orgUserId3);
|
||||
Assert.DoesNotContain(result, p => p.OrganizationId == orgId4 && p.OrganizationUserId == orgUserId4);
|
||||
}
|
||||
}
|
@ -122,9 +122,6 @@ public class SingleOrgPolicyValidatorTests
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
|
||||
.Returns(new CommandResult());
|
||||
@ -148,161 +145,4 @@ public class SingleOrgPolicyValidatorTests
|
||||
.Received(1)
|
||||
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers(
|
||||
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg, false)] Policy policy,
|
||||
Guid savingUserId,
|
||||
Guid nonCompliantUserId,
|
||||
Organization organization, SutProvider<SingleOrgPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
|
||||
var compliantUser1 = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = organization.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = new Guid(),
|
||||
Email = "user1@example.com"
|
||||
};
|
||||
|
||||
var compliantUser2 = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = organization.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = new Guid(),
|
||||
Email = "user2@example.com"
|
||||
};
|
||||
|
||||
var nonCompliantUser = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = organization.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = nonCompliantUserId,
|
||||
Email = "user3@example.com"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([compliantUser1, compliantUser2, nonCompliantUser]);
|
||||
|
||||
var otherOrganizationUser = new OrganizationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = new Guid(),
|
||||
UserId = nonCompliantUserId,
|
||||
Status = OrganizationUserStatusType.Confirmed
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(nonCompliantUserId)))
|
||||
.Returns([otherOrganizationUser]);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
|
||||
.Returns(new CommandResult());
|
||||
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
|
||||
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
|
||||
.DidNotReceive()
|
||||
.RemoveUserAsync(policyUpdate.OrganizationId, compliantUser1.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
|
||||
.DidNotReceive()
|
||||
.RemoveUserAsync(policyUpdate.OrganizationId, compliantUser2.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
|
||||
.Received(1)
|
||||
.RemoveUserAsync(policyUpdate.OrganizationId, nonCompliantUser.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_WhenAccountDeprovisioningIsEnabled_ThenUsersAreRevoked(
|
||||
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.SingleOrg, false)] Policy policy,
|
||||
Guid savingUserId,
|
||||
Guid nonCompliantUserId,
|
||||
Organization organization, SutProvider<SingleOrgPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
|
||||
var compliantUser1 = new OrganizationUserUserDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = new Guid(),
|
||||
Email = "user1@example.com"
|
||||
};
|
||||
|
||||
var compliantUser2 = new OrganizationUserUserDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = new Guid(),
|
||||
Email = "user2@example.com"
|
||||
};
|
||||
|
||||
var nonCompliantUser = new OrganizationUserUserDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
UserId = nonCompliantUserId,
|
||||
Email = "user3@example.com"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([compliantUser1, compliantUser2, nonCompliantUser]);
|
||||
|
||||
var otherOrganizationUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = new Guid(),
|
||||
UserId = nonCompliantUserId,
|
||||
Status = OrganizationUserStatusType.Confirmed
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(nonCompliantUserId)))
|
||||
.Returns([otherOrganizationUser]);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
|
||||
.Returns(new CommandResult());
|
||||
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
|
||||
|
||||
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.Received()
|
||||
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
@ -24,7 +23,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidat
|
||||
public class TwoFactorAuthenticationPolicyValidatorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers(
|
||||
public async Task OnSaveSideEffectsAsync_GivenNonCompliantUsersWithoutMasterPassword_Throws(
|
||||
Organization organization,
|
||||
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
|
||||
@ -33,249 +32,6 @@ public class TwoFactorAuthenticationPolicyValidatorTests
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
var orgUserDetailUserInvited = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user1@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = false
|
||||
};
|
||||
var orgUserDetailUserAcceptedWith2FA = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Accepted,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user2@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = true
|
||||
};
|
||||
var orgUserDetailUserAcceptedWithout2FA = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Accepted,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user3@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = true
|
||||
};
|
||||
var orgUserDetailAdmin = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.Admin,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "admin@test.com",
|
||||
Name = "ADMIN",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = false
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns(new List<OrganizationUserUserDetails>
|
||||
{
|
||||
orgUserDetailUserInvited,
|
||||
orgUserDetailUserAcceptedWith2FA,
|
||||
orgUserDetailUserAcceptedWithout2FA,
|
||||
orgUserDetailAdmin
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
|
||||
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
|
||||
{
|
||||
(orgUserDetailUserInvited, false),
|
||||
(orgUserDetailUserAcceptedWith2FA, true),
|
||||
(orgUserDetailUserAcceptedWithout2FA, false),
|
||||
(orgUserDetailAdmin, false),
|
||||
});
|
||||
|
||||
var savingUserId = Guid.NewGuid();
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
|
||||
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
|
||||
|
||||
var removeOrganizationUserCommand = sutProvider.GetDependency<IRemoveOrganizationUserCommand>();
|
||||
|
||||
await removeOrganizationUserCommand.Received()
|
||||
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IMailService>().Received()
|
||||
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email);
|
||||
|
||||
await removeOrganizationUserCommand.DidNotReceive()
|
||||
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IMailService>().DidNotReceive()
|
||||
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email);
|
||||
await removeOrganizationUserCommand.DidNotReceive()
|
||||
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IMailService>().DidNotReceive()
|
||||
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email);
|
||||
await removeOrganizationUserCommand.DidNotReceive()
|
||||
.RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId);
|
||||
await sutProvider.GetDependency<IMailService>().DidNotReceive()
|
||||
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_UsersToBeRemovedDontHaveMasterPasswords_Throws(
|
||||
Organization organization,
|
||||
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
|
||||
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
|
||||
var orgUserDetailUserWith2FAAndMP = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user1@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = true
|
||||
};
|
||||
var orgUserDetailUserWith2FANoMP = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user2@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = false
|
||||
};
|
||||
var orgUserDetailUserWithout2FA = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "user3@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = false
|
||||
};
|
||||
var orgUserDetailAdmin = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.Admin,
|
||||
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
|
||||
Email = "admin@test.com",
|
||||
Name = "ADMIN",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = false
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policy.OrganizationId)
|
||||
.Returns(new List<OrganizationUserUserDetails>
|
||||
{
|
||||
orgUserDetailUserWith2FAAndMP,
|
||||
orgUserDetailUserWith2FANoMP,
|
||||
orgUserDetailUserWithout2FA,
|
||||
orgUserDetailAdmin
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids =>
|
||||
ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value)
|
||||
&& ids.Contains(orgUserDetailUserWithout2FA.UserId.Value)
|
||||
&& ids.Contains(orgUserDetailAdmin.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool hasTwoFactor)>()
|
||||
{
|
||||
(orgUserDetailUserWith2FANoMP.UserId.Value, true),
|
||||
(orgUserDetailUserWithout2FA.UserId.Value, false),
|
||||
(orgUserDetailAdmin.UserId.Value, false),
|
||||
});
|
||||
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy));
|
||||
|
||||
Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, badRequestException.Message);
|
||||
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>().DidNotReceiveWithAnyArgs()
|
||||
.RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_GivenUpdateTo2faPolicy_WhenAccountProvisioningIsDisabled_ThenRevokeUserCommandShouldNotBeCalled(
|
||||
Organization organization,
|
||||
[PolicyUpdate(PolicyType.TwoFactorAuthentication)]
|
||||
PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)]
|
||||
Policy policy,
|
||||
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(false);
|
||||
|
||||
var orgUserDetailUserAcceptedWithout2Fa = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Accepted,
|
||||
Type = OrganizationUserType.User,
|
||||
Email = "user3@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = true
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns(new List<OrganizationUserUserDetails>
|
||||
{
|
||||
orgUserDetailUserAcceptedWithout2Fa
|
||||
});
|
||||
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
|
||||
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
|
||||
{
|
||||
(orgUserDetailUserAcceptedWithout2Fa, false),
|
||||
});
|
||||
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
|
||||
|
||||
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.DidNotReceive()
|
||||
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_GivenUpdateTo2faPolicy_WhenAccountProvisioningIsEnabledAndUserDoesNotHaveMasterPassword_ThenNonCompliantMembersErrorMessageWillReturn(
|
||||
Organization organization,
|
||||
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
|
||||
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
@ -304,7 +60,7 @@ public class TwoFactorAuthenticationPolicyValidatorTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_WhenAccountProvisioningIsEnabledAndUserHasMasterPassword_ThenUserWillBeRevoked(
|
||||
public async Task OnSaveSideEffectsAsync_RevokesNonCompliantUsers(
|
||||
Organization organization,
|
||||
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
|
||||
@ -313,10 +69,6 @@ public class TwoFactorAuthenticationPolicyValidatorTests
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
|
@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
@ -9,32 +9,49 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
public class EventIntegrationHandlerTests
|
||||
{
|
||||
private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#";
|
||||
private const string _templateWithOrganization = "Org: #OrganizationName#";
|
||||
private const string _templateWithUser = "#UserName#, #UserEmail#";
|
||||
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#";
|
||||
private const string _url = "https://localhost";
|
||||
private const string _url2 = "https://example.com";
|
||||
private readonly IIntegrationPublisher _integrationPublisher = Substitute.For<IIntegrationPublisher>();
|
||||
|
||||
private SutProvider<TestIntegrationEventHandlerBase> GetSutProvider(
|
||||
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
|
||||
List<OrganizationIntegrationConfigurationDetails> configurations)
|
||||
{
|
||||
var configurationRepository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
|
||||
configurationRepository.GetConfigurationDetailsAsync(Arg.Any<Guid>(),
|
||||
IntegrationType.Webhook, Arg.Any<EventType>()).Returns(configurations);
|
||||
|
||||
return new SutProvider<TestIntegrationEventHandlerBase>()
|
||||
return new SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>()
|
||||
.SetDependency(configurationRepository)
|
||||
.SetDependency(_integrationPublisher)
|
||||
.SetDependency(IntegrationType.Webhook)
|
||||
.Create();
|
||||
}
|
||||
|
||||
private static IntegrationMessage<WebhookIntegrationConfigurationDetails> expectedMessage(string template)
|
||||
{
|
||||
return new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
|
||||
{
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
Configuration = new WebhookIntegrationConfigurationDetails(_url),
|
||||
RenderedTemplate = template,
|
||||
RetryCount = 0,
|
||||
DelayUntilDate = null
|
||||
};
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
|
||||
{
|
||||
return [];
|
||||
@ -58,7 +75,7 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
config.Template = template;
|
||||
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config2.Configuration = null;
|
||||
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
|
||||
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url2 });
|
||||
config2.Template = template;
|
||||
|
||||
return [config, config2];
|
||||
@ -70,7 +87,7 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
Assert.Empty(sutProvider.Sut.CapturedCalls);
|
||||
Assert.Empty(_integrationPublisher.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -80,11 +97,12 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
Assert.Single(sutProvider.Sut.CapturedCalls);
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||
);
|
||||
|
||||
var expectedTemplate = $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}";
|
||||
|
||||
Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate);
|
||||
Assert.Single(_integrationPublisher.ReceivedCalls());
|
||||
await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
}
|
||||
@ -100,10 +118,10 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
Assert.Single(sutProvider.Sut.CapturedCalls);
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}");
|
||||
|
||||
var expectedTemplate = $"{user.Name}, {user.Email}";
|
||||
Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate);
|
||||
Assert.Single(_integrationPublisher.ReceivedCalls());
|
||||
await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty);
|
||||
}
|
||||
@ -118,10 +136,12 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(organization);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
Assert.Single(sutProvider.Sut.CapturedCalls);
|
||||
Assert.Single(_integrationPublisher.ReceivedCalls());
|
||||
|
||||
var expectedTemplate = $"Org: {organization.Name}";
|
||||
Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate);
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}");
|
||||
|
||||
Assert.Single(_integrationPublisher.ReceivedCalls());
|
||||
await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty);
|
||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
}
|
||||
@ -137,10 +157,10 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
Assert.Single(sutProvider.Sut.CapturedCalls);
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}");
|
||||
|
||||
var expectedTemplate = $"{user.Name}, {user.Email}";
|
||||
Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate);
|
||||
Assert.Single(_integrationPublisher.ReceivedCalls());
|
||||
await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty);
|
||||
}
|
||||
@ -151,7 +171,7 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
Assert.Empty(sutProvider.Sut.CapturedCalls);
|
||||
Assert.Empty(_integrationPublisher.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -161,15 +181,12 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
|
||||
Assert.Equal(eventMessages.Count, sutProvider.Sut.CapturedCalls.Count);
|
||||
var index = 0;
|
||||
foreach (var call in sutProvider.Sut.CapturedCalls)
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
var expected = eventMessages[index];
|
||||
var expectedTemplate = $"Date: {expected.Date}, Type: {expected.Type}, UserId: {expected.UserId}";
|
||||
|
||||
Assert.Equal(expectedTemplate, call.RenderedTemplate);
|
||||
index++;
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||
);
|
||||
await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,39 +198,15 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
|
||||
Assert.Equal(eventMessages.Count * 2, sutProvider.Sut.CapturedCalls.Count);
|
||||
|
||||
var capturedCalls = sutProvider.Sut.CapturedCalls.GetEnumerator();
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
var expectedTemplate = $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}";
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||
);
|
||||
await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
|
||||
|
||||
Assert.True(capturedCalls.MoveNext());
|
||||
var call = capturedCalls.Current;
|
||||
Assert.Equal(expectedTemplate, call.RenderedTemplate);
|
||||
|
||||
Assert.True(capturedCalls.MoveNext());
|
||||
call = capturedCalls.Current;
|
||||
Assert.Equal(expectedTemplate, call.RenderedTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestIntegrationEventHandlerBase : IntegrationEventHandlerBase
|
||||
{
|
||||
public TestIntegrationEventHandlerBase(IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationIntegrationConfigurationRepository configurationRepository)
|
||||
: base(userRepository, organizationRepository, configurationRepository)
|
||||
{ }
|
||||
|
||||
public List<(JsonObject MergedConfiguration, string RenderedTemplate)> CapturedCalls { get; } = new();
|
||||
|
||||
protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook;
|
||||
|
||||
protected override Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate)
|
||||
{
|
||||
CapturedCalls.Add((mergedConfiguration, renderedTemplate));
|
||||
return Task.CompletedTask;
|
||||
expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2);
|
||||
await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
public class IntegrationHandlerTests
|
||||
{
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage()
|
||||
{
|
||||
var sut = new TestIntegrationHandler();
|
||||
var expected = new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RenderedTemplate = "Template",
|
||||
DelayUntilDate = null,
|
||||
RetryCount = 0
|
||||
};
|
||||
|
||||
var result = await sut.HandleAsync(expected.ToJson());
|
||||
var typedResult = Assert.IsType<IntegrationMessage<WebhookIntegrationConfigurationDetails>>(result.Message);
|
||||
|
||||
Assert.Equal(expected.Configuration, typedResult.Configuration);
|
||||
Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate);
|
||||
Assert.Equal(expected.IntegrationType, typedResult.IntegrationType);
|
||||
}
|
||||
|
||||
private class TestIntegrationHandler : IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
public override Task<IntegrationHandlerResult> HandleAsync(
|
||||
IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var result = new IntegrationHandlerResult(success: true, message: message);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
}
|
30
test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs
Normal file
30
test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Bit.Core.Enums;
|
||||
using Xunit;
|
||||
|
||||
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()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => IntegrationType.CloudBillingSync.ToRoutingKey());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRoutingKey_Scim_ThrowsException()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => IntegrationType.Scim.ToRoutingKey());
|
||||
}
|
||||
}
|
@ -9,7 +9,6 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
@ -22,9 +21,6 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -44,7 +40,6 @@ public class OrganizationServiceTests
|
||||
{
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SignupClientAsync_Succeeds(
|
||||
OrganizationSignup signup,
|
||||
@ -638,9 +633,8 @@ public class OrganizationServiceTests
|
||||
.UpdateSubscriptionAsync(Arg.Any<SecretsManagerSubscriptionUpdate>())
|
||||
.ReturnsForAnyArgs(Task.FromResult(0)).AndDoes(x => organization.SmSeats += invitedSmUsers);
|
||||
|
||||
// Throw error at the end of the try block
|
||||
sutProvider.GetDependency<IReferenceEventService>().RaiseEventAsync(default)
|
||||
.ThrowsForAnyArgs<BadRequestException>();
|
||||
sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
|
||||
.SendInvitesAsync(Arg.Any<SendInvitesRequest>()).ThrowsAsync<Exception>();
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
@ -1,181 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SlackEventHandlerTests
|
||||
{
|
||||
private readonly IOrganizationIntegrationConfigurationRepository _repository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
|
||||
private readonly ISlackService _slackService = Substitute.For<ISlackService>();
|
||||
private readonly string _channelId = "C12345";
|
||||
private readonly string _channelId2 = "C67890";
|
||||
private readonly string _token = "xoxb-test-token";
|
||||
private readonly string _token2 = "xoxb-another-test-token";
|
||||
|
||||
private SutProvider<SlackEventHandler> GetSutProvider(
|
||||
List<OrganizationIntegrationConfigurationDetails> integrationConfigurations)
|
||||
{
|
||||
_repository.GetConfigurationDetailsAsync(Arg.Any<Guid>(),
|
||||
IntegrationType.Slack, Arg.Any<EventType>())
|
||||
.Returns(integrationConfigurations);
|
||||
|
||||
return new SutProvider<SlackEventHandler>()
|
||||
.SetDependency(_repository)
|
||||
.SetDependency(_slackService)
|
||||
.Create();
|
||||
}
|
||||
|
||||
private List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private List<OrganizationIntegrationConfigurationDetails> OneConfiguration()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = JsonSerializer.Serialize(new { token = _token });
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId });
|
||||
config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
|
||||
|
||||
return [config];
|
||||
}
|
||||
|
||||
private List<OrganizationIntegrationConfigurationDetails> TwoConfigurations()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = JsonSerializer.Serialize(new { token = _token });
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId });
|
||||
config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
|
||||
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config2.Configuration = JsonSerializer.Serialize(new { token = _token2 });
|
||||
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId2 });
|
||||
config2.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
|
||||
|
||||
return [config, config2];
|
||||
}
|
||||
|
||||
private List<OrganizationIntegrationConfigurationDetails> WrongConfiguration()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = JsonSerializer.Serialize(new { });
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { });
|
||||
config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
|
||||
|
||||
return [config];
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<ISlackService>().DidNotReceiveWithAnyArgs();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_OneConfiguration_SendsEventViaSlackService(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_TwoConfigurations_SendsMultipleEvents(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(TwoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
|
||||
);
|
||||
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token2)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId2))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_WrongConfiguration_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(WrongConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<ISlackService>().DidNotReceiveWithAnyArgs();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_OneConfiguration_SendsEventsViaSlackService(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
|
||||
var received = sutProvider.GetDependency<ISlackService>().ReceivedCalls();
|
||||
using var calls = received.GetEnumerator();
|
||||
|
||||
Assert.Equal(eventMessages.Count, received.Count());
|
||||
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
Assert.True(calls.MoveNext());
|
||||
var arguments = calls.Current.GetArguments();
|
||||
Assert.Equal(_token, arguments[0] as string);
|
||||
Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}",
|
||||
arguments[1] as string);
|
||||
Assert.Equal(_channelId, arguments[2] as string);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_TwoConfigurations_SendsMultipleEvents(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(TwoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
|
||||
var received = sutProvider.GetDependency<ISlackService>().ReceivedCalls();
|
||||
using var calls = received.GetEnumerator();
|
||||
|
||||
Assert.Equal(eventMessages.Count * 2, received.Count());
|
||||
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
Assert.True(calls.MoveNext());
|
||||
var arguments = calls.Current.GetArguments();
|
||||
Assert.Equal(_token, arguments[0] as string);
|
||||
Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}",
|
||||
arguments[1] as string);
|
||||
Assert.Equal(_channelId, arguments[2] as string);
|
||||
|
||||
Assert.True(calls.MoveNext());
|
||||
var arguments2 = calls.Current.GetArguments();
|
||||
Assert.Equal(_token2, arguments2[0] as string);
|
||||
Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}",
|
||||
arguments2[1] as string);
|
||||
Assert.Equal(_channelId2, arguments2[2] as string);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SlackIntegrationHandlerTests
|
||||
{
|
||||
private readonly ISlackService _slackService = Substitute.For<ISlackService>();
|
||||
private readonly string _channelId = "C12345";
|
||||
private readonly string _token = "xoxb-test-token";
|
||||
|
||||
private SutProvider<SlackIntegrationHandler> GetSutProvider()
|
||||
{
|
||||
return new SutProvider<SlackIntegrationHandler>()
|
||||
.SetDependency(_slackService)
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<SlackIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token);
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
|
||||
);
|
||||
}
|
||||
}
|
@ -1,235 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Bit.Test.Common.MockedHttpClient;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class WebhookEventHandlerTests
|
||||
{
|
||||
private readonly MockedHttpMessageHandler _handler;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private const string _template =
|
||||
"""
|
||||
{
|
||||
"Date": "#Date#",
|
||||
"Type": "#Type#",
|
||||
"UserId": "#UserId#"
|
||||
}
|
||||
""";
|
||||
private const string _webhookUrl = "http://localhost/test/event";
|
||||
private const string _webhookUrl2 = "http://localhost/another/event";
|
||||
|
||||
public WebhookEventHandlerTests()
|
||||
{
|
||||
_handler = new MockedHttpMessageHandler();
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
_httpClient = _handler.ToHttpClient();
|
||||
}
|
||||
|
||||
private SutProvider<WebhookEventHandler> GetSutProvider(
|
||||
List<OrganizationIntegrationConfigurationDetails> configurations)
|
||||
{
|
||||
var clientFactory = Substitute.For<IHttpClientFactory>();
|
||||
clientFactory.CreateClient(WebhookEventHandler.HttpClientName).Returns(_httpClient);
|
||||
|
||||
var repository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
|
||||
repository.GetConfigurationDetailsAsync(Arg.Any<Guid>(),
|
||||
IntegrationType.Webhook, Arg.Any<EventType>()).Returns(configurations);
|
||||
|
||||
return new SutProvider<WebhookEventHandler>()
|
||||
.SetDependency(repository)
|
||||
.SetDependency(clientFactory)
|
||||
.Create();
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> OneConfiguration()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl });
|
||||
config.Template = _template;
|
||||
|
||||
return [config];
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> TwoConfigurations()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl });
|
||||
config.Template = _template;
|
||||
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config2.Configuration = null;
|
||||
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl2 });
|
||||
config2.Template = _template;
|
||||
|
||||
return [config, config2];
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> WrongConfiguration()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { error = string.Empty });
|
||||
config.Template = _template;
|
||||
|
||||
return [config];
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Empty(_handler.CapturedRequests);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_OneConfiguration_PostsEventToUrl(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
var returned = await request.Content.ReadFromJsonAsync<MockEvent>();
|
||||
var expected = MockEvent.From(eventMessage);
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
AssertHelper.AssertPropertyEqual(expected, returned);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_WrongConfigurations_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(WrongConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Empty(_handler.CapturedRequests);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_NoConfigurations_DoesNothing(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Empty(_handler.CapturedRequests);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_OneConfiguration_PostsEventsToUrl(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Equal(eventMessages.Count, _handler.CapturedRequests.Count);
|
||||
var index = 0;
|
||||
foreach (var request in _handler.CapturedRequests)
|
||||
{
|
||||
Assert.NotNull(request);
|
||||
var returned = await request.Content.ReadFromJsonAsync<MockEvent>();
|
||||
var expected = MockEvent.From(eventMessages[index]);
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
AssertHelper.AssertPropertyEqual(expected, returned);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_TwoConfigurations_PostsEventsToMultipleUrls(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(TwoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
using var capturedRequests = _handler.CapturedRequests.GetEnumerator();
|
||||
Assert.Equal(eventMessages.Count * 2, _handler.CapturedRequests.Count);
|
||||
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
var expected = MockEvent.From(eventMessage);
|
||||
|
||||
Assert.True(capturedRequests.MoveNext());
|
||||
var request = capturedRequests.Current;
|
||||
Assert.NotNull(request);
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
var returned = await request.Content.ReadFromJsonAsync<MockEvent>();
|
||||
AssertHelper.AssertPropertyEqual(expected, returned);
|
||||
|
||||
Assert.True(capturedRequests.MoveNext());
|
||||
request = capturedRequests.Current;
|
||||
Assert.NotNull(request);
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl2, request.RequestUri.ToString());
|
||||
returned = await request.Content.ReadFromJsonAsync<MockEvent>();
|
||||
AssertHelper.AssertPropertyEqual(expected, returned);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MockEvent(string date, string type, string userId)
|
||||
{
|
||||
public string Date { get; set; } = date;
|
||||
public string Type { get; set; } = type;
|
||||
public string UserId { get; set; } = userId;
|
||||
|
||||
public static MockEvent From(EventMessage eventMessage)
|
||||
{
|
||||
return new MockEvent(
|
||||
eventMessage.Date.ToString(),
|
||||
eventMessage.Type.ToString(),
|
||||
eventMessage.UserId.ToString()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
using System.Net;
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Bit.Test.Common.MockedHttpClient;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class WebhookIntegrationHandlerTests
|
||||
{
|
||||
private readonly MockedHttpMessageHandler _handler;
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string _webhookUrl = "http://localhost/test/event";
|
||||
|
||||
public WebhookIntegrationHandlerTests()
|
||||
{
|
||||
_handler = new MockedHttpMessageHandler();
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
_httpClient = _handler.ToHttpClient();
|
||||
}
|
||||
|
||||
private SutProvider<WebhookIntegrationHandler> GetSutProvider()
|
||||
{
|
||||
var clientFactory = Substitute.For<IHttpClientFactory>();
|
||||
clientFactory.CreateClient(WebhookIntegrationHandler.HttpClientName).Returns(_httpClient);
|
||||
|
||||
return new SutProvider<WebhookIntegrationHandler>()
|
||||
.SetDependency(clientFactory)
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
var returned = await request.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsNotBeforUtc(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TooManyRequests)
|
||||
.WithHeader("Retry-After", "60")
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.True(result.DelayUntilDate.HasValue);
|
||||
Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsNotBeforUtc(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TooManyRequests)
|
||||
.WithHeader("Retry-After", DateTime.UtcNow.AddSeconds(60).ToString("r")) // "r" is the round-trip format: RFC1123
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.True(result.DelayUntilDate.HasValue);
|
||||
Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.InternalServerError)
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.False(result.DelayUntilDate.HasValue);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TemporaryRedirect)
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.Null(result.DelayUntilDate);
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
@ -76,18 +78,6 @@ public class IntegrationTemplateProcessorTests
|
||||
var expectedEmpty = "";
|
||||
|
||||
Assert.Equal(expectedEmpty, IntegrationTemplateProcessor.ReplaceTokens(emptyTemplate, eventMessage));
|
||||
Assert.Null(IntegrationTemplateProcessor.ReplaceTokens(null, eventMessage));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceTokens_DataObjectIsNull_ReturnsOriginalString()
|
||||
{
|
||||
var template = "Event #Type#, User (id: #UserId#).";
|
||||
var expected = "Event #Type#, User (id: #UserId#).";
|
||||
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, null);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -14,9 +14,6 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -57,10 +54,6 @@ public class RegisterUserCommandTests
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendWelcomeEmailAsync(user);
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>()
|
||||
.Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -85,10 +78,6 @@ public class RegisterUserCommandTests
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendWelcomeEmailAsync(Arg.Any<User>());
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>()
|
||||
.DidNotReceive()
|
||||
.RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
@ -117,10 +106,6 @@ public class RegisterUserCommandTests
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.Received(1)
|
||||
.CreateUserAsync(user, masterPasswordHash);
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>()
|
||||
.Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
|
||||
}
|
||||
|
||||
// Complex happy path test
|
||||
@ -215,17 +200,13 @@ public class RegisterUserCommandTests
|
||||
.Received(1)
|
||||
.SendWelcomeEmailAsync(user);
|
||||
}
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>()
|
||||
.Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.SignupInitiationPath == initiationPath));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
await sutProvider.GetDependency<IReferenceEventService>()
|
||||
// Even if user doesn't have reference data, we should send them welcome email
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.SignupInitiationPath == default));
|
||||
.SendWelcomeEmailAsync(user);
|
||||
}
|
||||
|
||||
Assert.True(result.Succeeded);
|
||||
@ -354,10 +335,6 @@ public class RegisterUserCommandTests
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendWelcomeEmailAsync(user);
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>()
|
||||
.Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.ReceiveMarketingEmails == receiveMarketingMaterials));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -424,10 +401,6 @@ public class RegisterUserCommandTests
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendWelcomeEmailAsync(user);
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>()
|
||||
.Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -501,10 +474,6 @@ public class RegisterUserCommandTests
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendWelcomeEmailAsync(user);
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>()
|
||||
.Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -599,10 +568,6 @@ public class RegisterUserCommandTests
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendWelcomeEmailAsync(user);
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>()
|
||||
.Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -3,13 +3,11 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.Billing.Stubs;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Braintree;
|
||||
@ -194,7 +192,7 @@ public class SubscriberServiceTests
|
||||
|
||||
await stripeAdapter
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SubscriptionCancelAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>()); ;
|
||||
.SubscriptionCancelAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -1028,7 +1026,7 @@ public class SubscriberServiceTests
|
||||
|
||||
stripeAdapter
|
||||
.PaymentMethodListAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
|
||||
.Returns(GetPaymentMethodsAsync(new List<Stripe.PaymentMethod>()));
|
||||
.Returns(GetPaymentMethodsAsync(new List<PaymentMethod>()));
|
||||
|
||||
await sutProvider.Sut.RemovePaymentSource(organization);
|
||||
|
||||
@ -1060,7 +1058,7 @@ public class SubscriberServiceTests
|
||||
|
||||
stripeAdapter
|
||||
.PaymentMethodListAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
|
||||
.Returns(GetPaymentMethodsAsync(new List<Stripe.PaymentMethod>
|
||||
.Returns(GetPaymentMethodsAsync(new List<PaymentMethod>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
@ -1085,8 +1083,8 @@ public class SubscriberServiceTests
|
||||
.PaymentMethodDetachAsync(cardId);
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<Stripe.PaymentMethod> GetPaymentMethodsAsync(
|
||||
IEnumerable<Stripe.PaymentMethod> paymentMethods)
|
||||
private static async IAsyncEnumerable<PaymentMethod> GetPaymentMethodsAsync(
|
||||
IEnumerable<PaymentMethod> paymentMethods)
|
||||
{
|
||||
foreach (var paymentMethod in paymentMethods)
|
||||
{
|
||||
@ -1597,14 +1595,22 @@ public class SubscriberServiceTests
|
||||
City = "Example Town",
|
||||
State = "NY"
|
||||
},
|
||||
TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] }
|
||||
TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] },
|
||||
Subscriptions = new StripeList<Subscription>
|
||||
{
|
||||
Data = [
|
||||
new Subscription
|
||||
{
|
||||
Id = provider.GatewaySubscriptionId,
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
var subscription = new Subscription { Items = new StripeList<SubscriptionItem>() };
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(Arg.Any<string>())
|
||||
.Returns(subscription);
|
||||
sutProvider.GetDependency<IAutomaticTaxFactory>().CreateAsync(Arg.Any<AutomaticTaxFactoryParameters>())
|
||||
.Returns(new FakeAutomaticTaxStrategy(true));
|
||||
|
||||
await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation);
|
||||
|
||||
@ -1622,6 +1628,98 @@ public class SubscriberServiceTests
|
||||
await stripeAdapter.Received(1).TaxIdCreateAsync(provider.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(
|
||||
options => options.Type == "us_ein" &&
|
||||
options.Value == taxInformation.TaxId));
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateTaxInformation_NonUser_ReverseCharge_MakesCorrectInvocations(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } };
|
||||
|
||||
stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId, Arg.Is<CustomerGetOptions>(
|
||||
options => options.Expand.Contains("tax_ids"))).Returns(customer);
|
||||
|
||||
var taxInformation = new TaxInformation(
|
||||
"CA",
|
||||
"12345",
|
||||
"123456789",
|
||||
"us_ein",
|
||||
"123 Example St.",
|
||||
null,
|
||||
"Example Town",
|
||||
"NY");
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.CustomerUpdateAsync(
|
||||
Arg.Is<string>(p => p == provider.GatewayCustomerId),
|
||||
Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Address.Country == "CA" &&
|
||||
options.Address.PostalCode == "12345" &&
|
||||
options.Address.Line1 == "123 Example St." &&
|
||||
options.Address.Line2 == null &&
|
||||
options.Address.City == "Example Town" &&
|
||||
options.Address.State == "NY"))
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = provider.GatewayCustomerId,
|
||||
Address = new Address
|
||||
{
|
||||
Country = "CA",
|
||||
PostalCode = "12345",
|
||||
Line1 = "123 Example St.",
|
||||
Line2 = null,
|
||||
City = "Example Town",
|
||||
State = "NY"
|
||||
},
|
||||
TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] },
|
||||
Subscriptions = new StripeList<Subscription>
|
||||
{
|
||||
Data = [
|
||||
new Subscription
|
||||
{
|
||||
Id = provider.GatewaySubscriptionId,
|
||||
CustomerId = provider.GatewayCustomerId,
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
var subscription = new Subscription { Items = new StripeList<SubscriptionItem>() };
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(Arg.Any<string>())
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||
|
||||
await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation);
|
||||
|
||||
await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
||||
options =>
|
||||
options.Address.Country == taxInformation.Country &&
|
||||
options.Address.PostalCode == taxInformation.PostalCode &&
|
||||
options.Address.Line1 == taxInformation.Line1 &&
|
||||
options.Address.Line2 == taxInformation.Line2 &&
|
||||
options.Address.City == taxInformation.City &&
|
||||
options.Address.State == taxInformation.State));
|
||||
|
||||
await stripeAdapter.Received(1).TaxIdDeleteAsync(provider.GatewayCustomerId, "tax_id_1");
|
||||
|
||||
await stripeAdapter.Received(1).TaxIdCreateAsync(provider.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(
|
||||
options => options.Type == "us_ein" &&
|
||||
options.Value == taxInformation.TaxId));
|
||||
|
||||
await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options => options.TaxExempt == StripeConstants.TaxExempt.Reverse));
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -0,0 +1,346 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Commands;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
using static Bit.Core.Billing.Tax.Commands.OrganizationTrialParameters;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Tax.Commands;
|
||||
|
||||
public class PreviewTaxAmountCommandTests
|
||||
{
|
||||
private readonly ILogger<PreviewTaxAmountCommand> _logger = Substitute.For<ILogger<PreviewTaxAmountCommand>>();
|
||||
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly ITaxService _taxService = Substitute.For<ITaxService>();
|
||||
|
||||
private readonly PreviewTaxAmountCommand _command;
|
||||
|
||||
public PreviewTaxAmountCommandTests()
|
||||
{
|
||||
_command = new PreviewTaxAmountCommand(_logger, _pricingClient, _stripeAdapter, _taxService);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_WithSeatBasedPasswordManagerPlan_GetsTaxAmount()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.Currency == "usd" &&
|
||||
options.CustomerDetails.Address.Country == "US" &&
|
||||
options.CustomerDetails.Address.PostalCode == "12345" &&
|
||||
options.SubscriptionDetails.Items.Count == 1 &&
|
||||
options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
|
||||
options.SubscriptionDetails.Items[0].Quantity == 1 &&
|
||||
options.AutomaticTax.Enabled == true
|
||||
))
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
var taxAmount = result.AsT0;
|
||||
Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_WithNonSeatBasedPasswordManagerPlan_GetsTaxAmount()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.FamiliesAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.Currency == "usd" &&
|
||||
options.CustomerDetails.Address.Country == "US" &&
|
||||
options.CustomerDetails.Address.PostalCode == "12345" &&
|
||||
options.SubscriptionDetails.Items.Count == 1 &&
|
||||
options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripePlanId &&
|
||||
options.SubscriptionDetails.Items[0].Quantity == 1 &&
|
||||
options.AutomaticTax.Enabled == true
|
||||
))
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
var taxAmount = result.AsT0;
|
||||
Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_WithSecretsManagerPlan_GetsTaxAmount()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.SecretsManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.Currency == "usd" &&
|
||||
options.CustomerDetails.Address.Country == "US" &&
|
||||
options.CustomerDetails.Address.PostalCode == "12345" &&
|
||||
options.SubscriptionDetails.Items.Count == 2 &&
|
||||
options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
|
||||
options.SubscriptionDetails.Items[0].Quantity == 1 &&
|
||||
options.SubscriptionDetails.Items[1].Price == plan.SecretsManager.StripeSeatPlanId &&
|
||||
options.SubscriptionDetails.Items[1].Quantity == 1 &&
|
||||
options.Coupon == StripeConstants.CouponIDs.SecretsManagerStandalone &&
|
||||
options.AutomaticTax.Enabled == true
|
||||
))
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
var taxAmount = result.AsT0;
|
||||
Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_NonUSWithoutTaxId_GetsTaxAmount()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "CA",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.Currency == "usd" &&
|
||||
options.CustomerDetails.Address.Country == "CA" &&
|
||||
options.CustomerDetails.Address.PostalCode == "12345" &&
|
||||
options.SubscriptionDetails.Items.Count == 1 &&
|
||||
options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
|
||||
options.SubscriptionDetails.Items[0].Quantity == 1 &&
|
||||
options.AutomaticTax.Enabled == false
|
||||
))
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
var taxAmount = result.AsT0;
|
||||
Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_NonUSWithTaxId_GetsTaxAmount()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "CA",
|
||||
PostalCode = "12345",
|
||||
TaxId = "123456789"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
_taxService.GetStripeTaxCode(parameters.TaxInformation.Country, parameters.TaxInformation.TaxId)
|
||||
.Returns("ca_st");
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.Currency == "usd" &&
|
||||
options.CustomerDetails.Address.Country == "CA" &&
|
||||
options.CustomerDetails.Address.PostalCode == "12345" &&
|
||||
options.CustomerDetails.TaxIds.Count == 1 &&
|
||||
options.CustomerDetails.TaxIds[0].Type == "ca_st" &&
|
||||
options.CustomerDetails.TaxIds[0].Value == "123456789" &&
|
||||
options.SubscriptionDetails.Items.Count == 1 &&
|
||||
options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
|
||||
options.SubscriptionDetails.Items[0].Quantity == 1 &&
|
||||
options.AutomaticTax.Enabled == true
|
||||
))
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
var taxAmount = result.AsT0;
|
||||
Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_NonUSWithTaxId_UnknownTaxIdType_BadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "CA",
|
||||
PostalCode = "12345",
|
||||
TaxId = "123456789"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
_taxService.GetStripeTaxCode(parameters.TaxInformation.Country, parameters.TaxInformation.TaxId)
|
||||
.Returns((string)null);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal(BillingErrorTranslationKeys.UnknownTaxIdType, badRequest.TranslationKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_CustomerTaxLocationInvalid_BadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Throws(new StripeException
|
||||
{
|
||||
StripeError = new StripeError { Code = StripeConstants.ErrorCodes.CustomerTaxLocationInvalid }
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal(BillingErrorTranslationKeys.CustomerTaxLocationInvalid, badRequest.TranslationKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_TaxIdInvalid_BadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Throws(new StripeException
|
||||
{
|
||||
StripeError = new StripeError { Code = StripeConstants.ErrorCodes.TaxIdInvalid }
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal(BillingErrorTranslationKeys.TaxIdInvalid, badRequest.TranslationKey);
|
||||
}
|
||||
}
|
@ -2,15 +2,15 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Services.Implementations;
|
||||
namespace Bit.Core.Test.Billing.Tax.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AutomaticTaxFactoryTests
|
@ -1,5 +1,5 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -7,7 +7,7 @@ using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Services.Implementations.AutomaticTax;
|
||||
namespace Bit.Core.Test.Billing.Tax.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class BusinessUseAutomaticTaxStrategyTests
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Stubs;
|
||||
namespace Bit.Core.Test.Billing.Tax.Services;
|
||||
|
||||
/// <param name="isAutomaticTaxEnabled">
|
||||
/// Whether the subscription options will have automatic tax enabled or not.
|
@ -1,5 +1,5 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -7,7 +7,7 @@ using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Services.Implementations.AutomaticTax;
|
||||
namespace Bit.Core.Test.Billing.Tax.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PersonalUseAutomaticTaxStrategyTests
|
@ -28,14 +28,17 @@ public static class OrganizationLicenseFileFixtures
|
||||
private const string Version15 =
|
||||
"{\n 'LicenseKey': 'myLicenseKey',\n 'InstallationId': '78900000-0000-0000-0000-000000000123',\n 'Id': '12300000-0000-0000-0000-000000000456',\n 'Name': 'myOrg',\n 'BillingEmail': 'myBillingEmail',\n 'BusinessName': 'myBusinessName',\n 'Enabled': true,\n 'Plan': 'myPlan',\n 'PlanType': 11,\n 'Seats': 10,\n 'MaxCollections': 2,\n 'UsePolicies': true,\n 'UseSso': true,\n 'UseKeyConnector': true,\n 'UseScim': true,\n 'UseGroups': true,\n 'UseEvents': true,\n 'UseDirectory': true,\n 'UseTotp': true,\n 'Use2fa': true,\n 'UseApi': true,\n 'UseResetPassword': true,\n 'MaxStorageGb': 100,\n 'SelfHost': true,\n 'UsersGetPremium': true,\n 'UseCustomPermissions': true,\n 'Version': 14,\n 'Issued': '2023-12-14T02:03:33.374297Z',\n 'Refresh': '2023-12-07T22:42:33.970597Z',\n 'Expires': '2023-12-21T02:03:33.374297Z',\n 'ExpirationWithoutGracePeriod': null,\n 'UsePasswordManager': true,\n 'UseSecretsManager': true,\n 'SmSeats': 5,\n 'SmServiceAccounts': 8,\n 'LimitCollectionCreationDeletion': true,\n 'AllowAdminAccessToAllCollectionItems': true,\n 'Trial': true,\n 'LicenseType': 1,\n 'Hash': 'EZl4IvJaa1E5mPmlfp4p5twAtlmaxlF1yoZzVYP4vog=',\n 'Signature': ''\n}";
|
||||
|
||||
private static readonly Dictionary<int, string> LicenseVersions = new() { { 12, Version12 }, { 13, Version13 }, { 14, Version14 }, { 15, Version15 } };
|
||||
private const string Version16 =
|
||||
"{\n'LicenseKey': 'myLicenseKey',\n'InstallationId': '78900000-0000-0000-0000-000000000123',\n'Id': '12300000-0000-0000-0000-000000000456',\n'Name': 'myOrg',\n'BillingEmail': 'myBillingEmail',\n'BusinessName': 'myBusinessName',\n'Enabled': true,\n'Plan': 'myPlan',\n'PlanType': 11,\n'Seats': 10,\n'MaxCollections': 2,\n'UsePolicies': true,\n'UseSso': true,\n'UseKeyConnector': true,\n'UseScim': true,\n'UseGroups': true,\n'UseEvents': true,\n'UseDirectory': true,\n'UseTotp': true,\n'Use2fa': true,\n'UseApi': true,\n'UseResetPassword': true,\n'MaxStorageGb': 100,\n'SelfHost': true,\n'UsersGetPremium': true,\n'UseCustomPermissions': true,\n'Version': 15,\n'Issued': '2025-05-16T20:50:09.036931Z',\n'Refresh': '2025-05-23T20:50:09.036931Z',\n'Expires': '2025-05-23T20:50:09.036931Z',\n'ExpirationWithoutGracePeriod': null,\n'UsePasswordManager': true,\n'UseSecretsManager': true,\n'SmSeats': 5,\n'SmServiceAccounts': 8,\n'UseRiskInsights': false,\n'LimitCollectionCreationDeletion': true,\n'AllowAdminAccessToAllCollectionItems': true,\n'Trial': true,\n'LicenseType': 1,\n'UseOrganizationDomains': true,\n'UseAdminSponsoredFamilies': false,\n'Hash': 'k3M9SpHKUo0TmuSnNipeZleCHxgcEycKRXYl9BAg30Q=',\n'Signature': '',\n'Token': null\n}";
|
||||
|
||||
private static readonly Dictionary<int, string> LicenseVersions = new() { { 12, Version12 }, { 13, Version13 }, { 14, Version14 }, { 15, Version15 }, { 16, Version16 } };
|
||||
|
||||
public static OrganizationLicense GetVersion(int licenseVersion)
|
||||
{
|
||||
if (!LicenseVersions.ContainsKey(licenseVersion))
|
||||
{
|
||||
throw new Exception(
|
||||
$"Cannot find serialized license version {licenseVersion}. You must add this to OrganizationLicenseFileFixtures when adding a new license version.");
|
||||
$"Cannot find serialized license version {licenseVersion}. You must add this to OrganizationLicenseFileFixtures when adding a new license version.");
|
||||
}
|
||||
|
||||
var json = LicenseVersions.GetValueOrDefault(licenseVersion).Replace("'", "\"");
|
||||
@ -76,6 +79,7 @@ public static class OrganizationLicenseFileFixtures
|
||||
MaxCollections = 2,
|
||||
UsePolicies = true,
|
||||
UseSso = true,
|
||||
UseOrganizationDomains = true,
|
||||
UseKeyConnector = true,
|
||||
UseScim = true,
|
||||
UseGroups = true,
|
||||
|
@ -1,7 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
@ -9,7 +8,6 @@ using Bit.Core.OrganizationFeatures.OrganizationLicenses;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@ -78,10 +76,8 @@ public class CloudGetOrganizationLicenseQueryTests
|
||||
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
|
||||
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);
|
||||
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
|
||||
sutProvider.GetDependency<ILicensingService>()
|
||||
.CreateOrganizationTokenAsync(organization, installationId, subInfo, plan.SecretsManager.MaxProjects)
|
||||
.CreateOrganizationTokenAsync(organization, installationId, subInfo)
|
||||
.Returns(token);
|
||||
|
||||
var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);
|
||||
|
@ -86,7 +86,8 @@ public class UpdateOrganizationLicenseCommandTests
|
||||
"Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType",
|
||||
"Hash", "Signature", "SignatureBytes", "InstallationId", "Expires",
|
||||
"ExpirationWithoutGracePeriod", "Token", "LimitCollectionCreationDeletion",
|
||||
"LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems") &&
|
||||
"LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems",
|
||||
"UseOrganizationDomains", "UseAdminSponsoredFamilies") &&
|
||||
// Same property but different name, use explicit mapping
|
||||
org.ExpirationDate == license.Expires));
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.Api.Requests;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Stubs;
|
||||
using Bit.Core.Test.Billing.Tax.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
|
@ -2,8 +2,11 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@ -23,7 +26,6 @@ using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@ -313,7 +315,6 @@ public class UserServiceTests
|
||||
sutProvider.GetDependency<IPaymentService>(),
|
||||
sutProvider.GetDependency<IPolicyRepository>(),
|
||||
sutProvider.GetDependency<IPolicyService>(),
|
||||
sutProvider.GetDependency<IReferenceEventService>(),
|
||||
sutProvider.GetDependency<IFido2>(),
|
||||
sutProvider.GetDependency<ICurrentContext>(),
|
||||
sutProvider.GetDependency<IGlobalSettings>(),
|
||||
@ -326,7 +327,8 @@ public class UserServiceTests
|
||||
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
|
||||
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
|
||||
sutProvider.GetDependency<IDistributedCache>()
|
||||
sutProvider.GetDependency<IDistributedCache>(),
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
);
|
||||
|
||||
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
|
||||
@ -343,27 +345,11 @@ public class UserServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningDisabled_ReturnsFalse(
|
||||
SutProvider<UserService> sutProvider, Guid userId)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(false);
|
||||
|
||||
var result = await sutProvider.Sut.IsClaimedByAnyOrganizationAsync(userId);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingEnabledOrganization_ReturnsTrue(
|
||||
public async Task IsClaimedByAnyOrganizationAsync_WithManagingEnabledOrganization_ReturnsTrue(
|
||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||
{
|
||||
organization.Enabled = true;
|
||||
organization.UseSso = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
organization.UseOrganizationDomains = true;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByVerifiedUserEmailDomainAsync(userId)
|
||||
@ -374,15 +360,11 @@ public class UserServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingDisabledOrganization_ReturnsFalse(
|
||||
public async Task IsClaimedByAnyOrganizationAsync_WithManagingDisabledOrganization_ReturnsFalse(
|
||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||
{
|
||||
organization.Enabled = false;
|
||||
organization.UseSso = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
organization.UseOrganizationDomains = true;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByVerifiedUserEmailDomainAsync(userId)
|
||||
@ -393,15 +375,11 @@ public class UserServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithOrganizationUseSsoFalse_ReturnsFalse(
|
||||
public async Task IsClaimedByAnyOrganizationAsync_WithOrganizationUseOrganizationDomaisFalse_ReturnsFalse(
|
||||
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
|
||||
{
|
||||
organization.Enabled = true;
|
||||
organization.UseSso = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
organization.UseOrganizationDomains = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByVerifiedUserEmailDomainAsync(userId)
|
||||
@ -412,100 +390,7 @@ public class UserServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RemovesUserFromOrganizationAndSendsEmail(
|
||||
SutProvider<UserService> sutProvider, User user, Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Email] = new() { Enabled = true }
|
||||
});
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
|
||||
.Returns(
|
||||
[
|
||||
new OrganizationUserPolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
PolicyEnabled = true
|
||||
}
|
||||
]);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
|
||||
.Received(1)
|
||||
.RemoveUserAsync(organization.Id, user.Id);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), user.Email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_UserHasOneProviderEnabled_DoesNotRemoveUserFromOrganization(
|
||||
SutProvider<UserService> sutProvider, User user, Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Email] = new() { Enabled = true },
|
||||
[TwoFactorProviderType.Remember] = new() { Enabled = true }
|
||||
});
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
|
||||
.Returns(
|
||||
[
|
||||
new OrganizationUserPolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||
PolicyEnabled = true
|
||||
}
|
||||
]);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(user)
|
||||
.Returns(true);
|
||||
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Remember] = new() { Enabled = true }
|
||||
}, JsonHelpers.LegacyEnumKeyResolver);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RemoveUserAsync(default, default);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail(
|
||||
public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail(
|
||||
SutProvider<UserService> sutProvider, User user,
|
||||
Organization organization1, Guid organizationUserId1,
|
||||
Organization organization2, Guid organizationUserId2)
|
||||
@ -518,9 +403,6 @@ public class UserServiceTests
|
||||
organization1.Enabled = organization2.Enabled = true;
|
||||
organization1.UseSso = organization2.UseSso = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
|
||||
.Returns(
|
||||
@ -583,7 +465,79 @@ public class UserServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_UserHasOneProviderEnabled_DoesNotRemoveUserFromOrganization(
|
||||
public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail(
|
||||
SutProvider<UserService> sutProvider, User user,
|
||||
Organization organization1, Guid organizationUserId1,
|
||||
Organization organization2, Guid organizationUserId2)
|
||||
{
|
||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Email] = new() { Enabled = true }
|
||||
});
|
||||
organization1.Enabled = organization2.Enabled = true;
|
||||
organization1.UseSso = organization2.UseSso = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization1.Id,
|
||||
OrganizationUserId = organizationUserId1,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
},
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization2.Id,
|
||||
OrganizationUserId = organizationUserId2,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
}
|
||||
]));
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetManyByIdsAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organization1.Id) && ids.Contains(organization2.Id)))
|
||||
.Returns(new[] { organization1, organization2 });
|
||||
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);
|
||||
|
||||
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
||||
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
|
||||
|
||||
// Revoke the user from the first organization
|
||||
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.Received(1)
|
||||
.RevokeNonCompliantOrganizationUsersAsync(
|
||||
Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization1.Id &&
|
||||
r.OrganizationUsers.First().Id == organizationUserId1 &&
|
||||
r.OrganizationUsers.First().OrganizationId == organization1.Id));
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization1.DisplayName(), user.Email);
|
||||
|
||||
// Remove the user from the second organization
|
||||
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.Received(1)
|
||||
.RevokeNonCompliantOrganizationUsersAsync(
|
||||
Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization2.Id &&
|
||||
r.OrganizationUsers.First().Id == organizationUserId2 &&
|
||||
r.OrganizationUsers.First().OrganizationId == organization2.Id));
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DisableTwoFactorProviderAsync_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization(
|
||||
SutProvider<UserService> sutProvider, User user, Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
@ -606,6 +560,9 @@ public class UserServiceTests
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(user)
|
||||
.Returns(true);
|
||||
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Remember] = new() { Enabled = true }
|
||||
@ -626,6 +583,53 @@ public class UserServiceTests
|
||||
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization(
|
||||
SutProvider<UserService> sutProvider, User user, Organization organization)
|
||||
{
|
||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Email] = new() { Enabled = true },
|
||||
[TwoFactorProviderType.Remember] = new() { Enabled = true }
|
||||
});
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
}
|
||||
]));
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(user)
|
||||
.Returns(true);
|
||||
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Remember] = new() { Enabled = true }
|
||||
}, JsonHelpers.LegacyEnumKeyResolver);
|
||||
|
||||
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
||||
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
|
||||
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.RevokeNonCompliantOrganizationUsersAsync(default);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled(
|
||||
SutProvider<UserService> sutProvider, string email, string secret)
|
||||
@ -904,7 +908,6 @@ public class UserServiceTests
|
||||
sutProvider.GetDependency<IPaymentService>(),
|
||||
sutProvider.GetDependency<IPolicyRepository>(),
|
||||
sutProvider.GetDependency<IPolicyService>(),
|
||||
sutProvider.GetDependency<IReferenceEventService>(),
|
||||
sutProvider.GetDependency<IFido2>(),
|
||||
sutProvider.GetDependency<ICurrentContext>(),
|
||||
sutProvider.GetDependency<IGlobalSettings>(),
|
||||
@ -917,7 +920,8 @@ public class UserServiceTests
|
||||
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
|
||||
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
|
||||
sutProvider.GetDependency<IDistributedCache>()
|
||||
sutProvider.GetDependency<IDistributedCache>(),
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,7 @@ using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.ImportFeatures;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
@ -49,7 +46,7 @@ public class ImportCiphersAsyncCommandTests
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
@ -77,7 +74,7 @@ public class ImportCiphersAsyncCommandTests
|
||||
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
@ -183,8 +180,6 @@ public class ImportCiphersAsyncCommandTests
|
||||
!cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization
|
||||
cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true)));
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
await sutProvider.GetDependency<IReferenceEventService>().Received(1).RaiseEventAsync(
|
||||
Arg.Is<ReferenceEvent>(e => e.Type == ReferenceEventType.VaultImported));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
118
test/Core.Test/Tools/Services/AnonymousSendCommandTests.cs
Normal file
118
test/Core.Test/Tools/Services/AnonymousSendCommandTests.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.SendFeatures.Commands;
|
||||
using Bit.Core.Tools.Services;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.Services;
|
||||
|
||||
public class AnonymousSendCommandTests
|
||||
{
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly ISendFileStorageService _sendFileStorageService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||
private readonly AnonymousSendCommand _anonymousSendCommand;
|
||||
|
||||
public AnonymousSendCommandTests()
|
||||
{
|
||||
_sendRepository = Substitute.For<ISendRepository>();
|
||||
_sendFileStorageService = Substitute.For<ISendFileStorageService>();
|
||||
_pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||
_sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
|
||||
|
||||
_anonymousSendCommand = new AnonymousSendCommand(
|
||||
_sendRepository,
|
||||
_sendFileStorageService,
|
||||
_pushNotificationService,
|
||||
_sendAuthorizationService);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSendFileDownloadUrlAsync_Success_ReturnsDownloadUrl()
|
||||
{
|
||||
// Arrange
|
||||
var send = new Send
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = SendType.File,
|
||||
AccessCount = 0,
|
||||
Data = JsonSerializer.Serialize(new { Id = "fileId123" })
|
||||
};
|
||||
var fileId = "fileId123";
|
||||
var password = "testPassword";
|
||||
var expectedUrl = "https://example.com/download";
|
||||
|
||||
_sendAuthorizationService
|
||||
.SendCanBeAccessed(send, password)
|
||||
.Returns(SendAccessResult.Granted);
|
||||
|
||||
_sendFileStorageService
|
||||
.GetSendFileDownloadUrlAsync(send, fileId)
|
||||
.Returns(expectedUrl);
|
||||
|
||||
// Act
|
||||
var result =
|
||||
await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedUrl, result.Item1);
|
||||
Assert.Equal(1, send.AccessCount);
|
||||
|
||||
await _sendRepository.Received(1).ReplaceAsync(send);
|
||||
await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSendFileDownloadUrlAsync_AccessDenied_ReturnsNullWithReasons()
|
||||
{
|
||||
// Arrange
|
||||
var send = new Send
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = SendType.File,
|
||||
AccessCount = 0
|
||||
};
|
||||
var fileId = "fileId123";
|
||||
var password = "wrongPassword";
|
||||
|
||||
_sendAuthorizationService
|
||||
.SendCanBeAccessed(send, password)
|
||||
.Returns(SendAccessResult.Denied);
|
||||
|
||||
// Act
|
||||
var result =
|
||||
await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.Item1);
|
||||
Assert.Equal(SendAccessResult.Denied, result.Item2);
|
||||
Assert.Equal(0, send.AccessCount);
|
||||
|
||||
await _sendRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default);
|
||||
await _pushNotificationService.DidNotReceiveWithAnyArgs().PushSyncSendUpdateAsync(default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSendFileDownloadUrlAsync_NotFileSend_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var send = new Send
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = SendType.Text
|
||||
};
|
||||
var fileId = "fileId123";
|
||||
var password = "testPassword";
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
_anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password));
|
||||
}
|
||||
}
|
1064
test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs
Normal file
1064
test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs
Normal file
File diff suppressed because it is too large
Load Diff
135
test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs
Normal file
135
test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs
Normal file
@ -0,0 +1,135 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.SendFeatures.Queries;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.Services;
|
||||
|
||||
public class SendAuthenticationQueryTests
|
||||
{
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly SendAuthenticationQuery _sendAuthenticationQuery;
|
||||
|
||||
public SendAuthenticationQueryTests()
|
||||
{
|
||||
_sendRepository = Substitute.For<ISendRepository>();
|
||||
_sendAuthenticationQuery = new SendAuthenticationQuery(_sendRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullRepository_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new SendAuthenticationQuery(null));
|
||||
Assert.Equal("sendRepository", exception.ParamName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AuthenticationMethodTestCases))]
|
||||
public async Task GetAuthenticationMethod_ReturnsExpectedAuthenticationMethod(Send? send, Type expectedType)
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||
|
||||
// Assert
|
||||
Assert.IsType(expectedType, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(EmailParsingTestCases))]
|
||||
public async Task GetAuthenticationMethod_WithEmails_ParsesEmailsCorrectly(string emailString, string[] expectedEmails)
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||
|
||||
// Assert
|
||||
var emailOtp = Assert.IsType<EmailOtp>(result);
|
||||
Assert.Equal(expectedEmails, emailOtp.Emails);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAuthenticationMethod_WithBothEmailsAndPassword_ReturnsEmailOtp()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: "hashedpassword");
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<EmailOtp>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAuthenticationMethod_CallsRepositoryWithCorrectSendId()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||
|
||||
// Assert
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAuthenticationMethod_WhenRepositoryThrows_PropagatesException()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var expectedException = new InvalidOperationException("Repository error");
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(Task.FromException<Send?>(expectedException));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_sendAuthenticationQuery.GetAuthenticationMethod(sendId));
|
||||
Assert.Same(expectedException, exception);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> AuthenticationMethodTestCases()
|
||||
{
|
||||
yield return new object[] { null, typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: null), typeof(EmailOtp) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: "hashedpassword"), typeof(ResourcePassword) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null), typeof(NotAuthenticated) };
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> EmailParsingTestCases()
|
||||
{
|
||||
yield return new object[] { "test@example.com", new[] { "test@example.com" } };
|
||||
yield return new object[] { "test1@example.com,test2@example.com", new[] { "test1@example.com", "test2@example.com" } };
|
||||
yield return new object[] { " test@example.com , other@example.com ", new[] { "test@example.com", "other@example.com" } };
|
||||
yield return new object[] { "test@example.com,,other@example.com", new[] { "test@example.com", "other@example.com" } };
|
||||
yield return new object[] { " , test@example.com, ,other@example.com, ", new[] { "test@example.com", "other@example.com" } };
|
||||
}
|
||||
|
||||
private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password)
|
||||
{
|
||||
return new Send
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AccessCount = accessCount,
|
||||
MaxAccessCount = maxAccessCount,
|
||||
Emails = emails,
|
||||
Password = password
|
||||
};
|
||||
}
|
||||
}
|
168
test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs
Normal file
168
test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs
Normal file
@ -0,0 +1,168 @@
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.Services;
|
||||
|
||||
public class SendAuthorizationServiceTests
|
||||
{
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly IPasswordHasher<Bit.Core.Entities.User> _passwordHasher;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly SendAuthorizationService _sendAuthorizationService;
|
||||
|
||||
public SendAuthorizationServiceTests()
|
||||
{
|
||||
_sendRepository = Substitute.For<ISendRepository>();
|
||||
_passwordHasher = Substitute.For<IPasswordHasher<Bit.Core.Entities.User>>();
|
||||
_pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||
|
||||
_sendAuthorizationService = new SendAuthorizationService(
|
||||
_sendRepository,
|
||||
_passwordHasher,
|
||||
_pushNotificationService);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void SendCanBeAccessed_Success_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var send = new Send
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = Guid.NewGuid(),
|
||||
MaxAccessCount = 10,
|
||||
AccessCount = 5,
|
||||
ExpirationDate = DateTime.UtcNow.AddYears(1),
|
||||
DeletionDate = DateTime.UtcNow.AddYears(1),
|
||||
Disabled = false,
|
||||
Password = "hashedPassword123"
|
||||
};
|
||||
|
||||
const string password = "TEST";
|
||||
|
||||
_passwordHasher
|
||||
.VerifyHashedPassword(Arg.Any<Bit.Core.Entities.User>(), send.Password, password)
|
||||
.Returns(PasswordVerificationResult.Success);
|
||||
|
||||
// Act
|
||||
var result =
|
||||
_sendAuthorizationService.SendCanBeAccessed(send, password);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(SendAccessResult.Granted, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SendCanBeAccessed_NullMaxAccess_Success()
|
||||
{
|
||||
// Arrange
|
||||
var send = new Send
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = Guid.NewGuid(),
|
||||
MaxAccessCount = null,
|
||||
AccessCount = 5,
|
||||
ExpirationDate = DateTime.UtcNow.AddYears(1),
|
||||
DeletionDate = DateTime.UtcNow.AddYears(1),
|
||||
Disabled = false,
|
||||
Password = "hashedPassword123"
|
||||
};
|
||||
|
||||
const string password = "TEST";
|
||||
|
||||
_passwordHasher
|
||||
.VerifyHashedPassword(Arg.Any<Bit.Core.Entities.User>(), send.Password, password)
|
||||
.Returns(PasswordVerificationResult.Success);
|
||||
|
||||
// Act
|
||||
var result = _sendAuthorizationService.SendCanBeAccessed(send, password);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(SendAccessResult.Granted, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SendCanBeAccessed_NullSend_DoesNotGrantAccess()
|
||||
{
|
||||
// Arrange
|
||||
_passwordHasher
|
||||
.VerifyHashedPassword(Arg.Any<Bit.Core.Entities.User>(), "TEST", "TEST")
|
||||
.Returns(PasswordVerificationResult.Success);
|
||||
|
||||
// Act
|
||||
var result =
|
||||
_sendAuthorizationService.SendCanBeAccessed(null, "TEST");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(SendAccessResult.Denied, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SendCanBeAccessed_RehashNeeded_RehashesPassword()
|
||||
{
|
||||
// Arrange
|
||||
var now = DateTime.UtcNow;
|
||||
var send = new Send
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = Guid.NewGuid(),
|
||||
MaxAccessCount = null,
|
||||
AccessCount = 5,
|
||||
ExpirationDate = now.AddYears(1),
|
||||
DeletionDate = now.AddYears(1),
|
||||
Disabled = false,
|
||||
Password = "TEST"
|
||||
};
|
||||
|
||||
_passwordHasher
|
||||
.VerifyHashedPassword(Arg.Any<Bit.Core.Entities.User>(), "TEST", "TEST")
|
||||
.Returns(PasswordVerificationResult.SuccessRehashNeeded);
|
||||
|
||||
// Act
|
||||
var result =
|
||||
_sendAuthorizationService.SendCanBeAccessed(send, "TEST");
|
||||
|
||||
// Assert
|
||||
_passwordHasher
|
||||
.Received(1)
|
||||
.HashPassword(Arg.Any<Bit.Core.Entities.User>(), "TEST");
|
||||
|
||||
Assert.Equal(SendAccessResult.Granted, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SendCanBeAccessed_VerifyFailed_PasswordInvalidReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var now = DateTime.UtcNow;
|
||||
var send = new Send
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = Guid.NewGuid(),
|
||||
MaxAccessCount = null,
|
||||
AccessCount = 5,
|
||||
ExpirationDate = now.AddYears(1),
|
||||
DeletionDate = now.AddYears(1),
|
||||
Disabled = false,
|
||||
Password = "TEST"
|
||||
};
|
||||
|
||||
_passwordHasher
|
||||
.VerifyHashedPassword(Arg.Any<Bit.Core.Entities.User>(), "TEST", "TEST")
|
||||
.Returns(PasswordVerificationResult.Failed);
|
||||
|
||||
// Act
|
||||
var result =
|
||||
_sendAuthorizationService.SendCanBeAccessed(send, "TEST");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(SendAccessResult.PasswordInvalid, result);
|
||||
}
|
||||
}
|
@ -1,867 +0,0 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.CurrentContextFixtures;
|
||||
using Bit.Core.Test.Entities;
|
||||
using Bit.Core.Test.Tools.AutoFixture.SendFixtures;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Tools.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[CurrentContextCustomize]
|
||||
[UserSendCustomize]
|
||||
public class SendServiceTests
|
||||
{
|
||||
private void SaveSendAsync_Setup(SendType sendType, bool disableSendPolicyAppliesToUser,
|
||||
SutProvider<SendService> sutProvider, Send send)
|
||||
{
|
||||
send.Id = default;
|
||||
send.Type = sendType;
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), PolicyType.DisableSend).Returns(disableSendPolicyAppliesToUser);
|
||||
}
|
||||
|
||||
// Disable Send policy check
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(SendType.File)]
|
||||
[BitAutoData(SendType.Text)]
|
||||
public async Task SaveSendAsync_DisableSend_Applies_throws(SendType sendType,
|
||||
SutProvider<SendService> sutProvider, Send send)
|
||||
{
|
||||
SaveSendAsync_Setup(sendType, disableSendPolicyAppliesToUser: true, sutProvider, send);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(SendType.File)]
|
||||
[BitAutoData(SendType.Text)]
|
||||
public async Task SaveSendAsync_DisableSend_DoesntApply_success(SendType sendType,
|
||||
SutProvider<SendService> sutProvider, Send send)
|
||||
{
|
||||
SaveSendAsync_Setup(sendType, disableSendPolicyAppliesToUser: false, sutProvider, send);
|
||||
|
||||
await sutProvider.Sut.SaveSendAsync(send);
|
||||
|
||||
await sutProvider.GetDependency<ISendRepository>().Received(1).CreateAsync(send);
|
||||
}
|
||||
|
||||
// Send Options Policy - Disable Hide Email check
|
||||
|
||||
private void SaveSendAsync_HideEmail_Setup(bool disableHideEmailAppliesToUser,
|
||||
SutProvider<SendService> sutProvider, Send send, Policy policy)
|
||||
{
|
||||
send.HideEmail = true;
|
||||
|
||||
var sendOptions = new SendOptionsPolicyData
|
||||
{
|
||||
DisableHideEmail = disableHideEmailAppliesToUser
|
||||
};
|
||||
policy.Data = JsonSerializer.Serialize(sendOptions, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>().GetPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), PolicyType.SendOptions).Returns(new List<OrganizationUserPolicyDetails>()
|
||||
{
|
||||
new() { PolicyType = policy.Type, PolicyData = policy.Data, OrganizationId = policy.OrganizationId, PolicyEnabled = policy.Enabled }
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(SendType.File)]
|
||||
[BitAutoData(SendType.Text)]
|
||||
public async Task SaveSendAsync_DisableHideEmail_Applies_throws(SendType sendType,
|
||||
SutProvider<SendService> sutProvider, Send send, Policy policy)
|
||||
{
|
||||
SaveSendAsync_Setup(sendType, false, sutProvider, send);
|
||||
SaveSendAsync_HideEmail_Setup(true, sutProvider, send, policy);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(SendType.File)]
|
||||
[BitAutoData(SendType.Text)]
|
||||
public async Task SaveSendAsync_DisableHideEmail_DoesntApply_success(SendType sendType,
|
||||
SutProvider<SendService> sutProvider, Send send, Policy policy)
|
||||
{
|
||||
SaveSendAsync_Setup(sendType, false, sutProvider, send);
|
||||
SaveSendAsync_HideEmail_Setup(false, sutProvider, send, policy);
|
||||
|
||||
await sutProvider.Sut.SaveSendAsync(send);
|
||||
|
||||
await sutProvider.GetDependency<ISendRepository>().Received(1).CreateAsync(send);
|
||||
}
|
||||
|
||||
// Disable Send policy check - vNext
|
||||
private void SaveSendAsync_Setup_vNext(SutProvider<SendService> sutProvider, Send send,
|
||||
DisableSendPolicyRequirement disableSendPolicyRequirement, SendOptionsPolicyRequirement sendOptionsPolicyRequirement)
|
||||
{
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<DisableSendPolicyRequirement>(send.UserId!.Value)
|
||||
.Returns(disableSendPolicyRequirement);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<SendOptionsPolicyRequirement>(send.UserId!.Value)
|
||||
.Returns(sendOptionsPolicyRequirement);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
|
||||
// Should not be called in these tests
|
||||
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), Arg.Any<PolicyType>()).ThrowsAsync<Exception>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(SendType.File)]
|
||||
[BitAutoData(SendType.Text)]
|
||||
public async Task SaveSendAsync_DisableSend_Applies_Throws_vNext(SendType sendType,
|
||||
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
|
||||
{
|
||||
send.Type = sendType;
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement { DisableSend = true }, new SendOptionsPolicyRequirement());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send));
|
||||
Assert.Contains("Due to an Enterprise Policy, you are only able to delete an existing Send.",
|
||||
exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(SendType.File)]
|
||||
[BitAutoData(SendType.Text)]
|
||||
public async Task SaveSendAsync_DisableSend_DoesntApply_Success_vNext(SendType sendType,
|
||||
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
|
||||
{
|
||||
send.Type = sendType;
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement());
|
||||
|
||||
await sutProvider.Sut.SaveSendAsync(send);
|
||||
|
||||
await sutProvider.GetDependency<ISendRepository>().Received(1).CreateAsync(send);
|
||||
}
|
||||
|
||||
// Send Options Policy - Disable Hide Email check
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(SendType.File)]
|
||||
[BitAutoData(SendType.Text)]
|
||||
public async Task SaveSendAsync_DisableHideEmail_Applies_Throws_vNext(SendType sendType,
|
||||
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
|
||||
{
|
||||
send.Type = sendType;
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true });
|
||||
send.HideEmail = true;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send));
|
||||
Assert.Contains("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(SendType.File)]
|
||||
[BitAutoData(SendType.Text)]
|
||||
public async Task SaveSendAsync_DisableHideEmail_Applies_ButEmailNotHidden_Success_vNext(SendType sendType,
|
||||
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
|
||||
{
|
||||
send.Type = sendType;
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true });
|
||||
send.HideEmail = false;
|
||||
|
||||
await sutProvider.Sut.SaveSendAsync(send);
|
||||
|
||||
await sutProvider.GetDependency<ISendRepository>().Received(1).CreateAsync(send);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(SendType.File)]
|
||||
[BitAutoData(SendType.Text)]
|
||||
public async Task SaveSendAsync_DisableHideEmail_DoesntApply_Success_vNext(SendType sendType,
|
||||
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
|
||||
{
|
||||
send.Type = sendType;
|
||||
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement());
|
||||
send.HideEmail = true;
|
||||
|
||||
await sutProvider.Sut.SaveSendAsync(send);
|
||||
|
||||
await sutProvider.GetDependency<ISendRepository>().Received(1).CreateAsync(send);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveSendAsync_ExistingSend_Updates(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
send.Id = Guid.NewGuid();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
await sutProvider.Sut.SaveSendAsync(send);
|
||||
|
||||
Assert.True(send.RevisionDate - now < TimeSpan.FromSeconds(1));
|
||||
|
||||
await sutProvider.GetDependency<ISendRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(send);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncSendUpdateAsync(send);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_TextType_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
send.Type = SendType.Text;
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 0)
|
||||
);
|
||||
|
||||
Assert.Contains("not of type \"file\"", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_EmptyFile_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
send.Type = SendType.File;
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 0)
|
||||
);
|
||||
|
||||
Assert.Contains("no file data", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_UserCannotAccessPremium_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
};
|
||||
|
||||
send.UserId = user.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(false);
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 1)
|
||||
);
|
||||
|
||||
Assert.Contains("must have premium", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_UserHasUnconfirmedEmail_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
EmailVerified = false,
|
||||
};
|
||||
|
||||
send.UserId = user.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 1)
|
||||
);
|
||||
|
||||
Assert.Contains("must confirm your email", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_UserCanAccessPremium_HasNoStorage_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
EmailVerified = true,
|
||||
Premium = true,
|
||||
MaxStorageGb = null,
|
||||
Storage = 0,
|
||||
};
|
||||
|
||||
send.UserId = user.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 1)
|
||||
);
|
||||
|
||||
Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_UserCanAccessPremium_StorageFull_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
EmailVerified = true,
|
||||
Premium = true,
|
||||
MaxStorageGb = 2,
|
||||
Storage = 2 * UserTests.Multiplier,
|
||||
};
|
||||
|
||||
send.UserId = user.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 1)
|
||||
);
|
||||
|
||||
Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsSelfHosted_GiantFile_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
EmailVerified = true,
|
||||
Premium = false,
|
||||
};
|
||||
|
||||
send.UserId = user.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.SelfHosted = true;
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 11000 * UserTests.Multiplier)
|
||||
);
|
||||
|
||||
Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsNotSelfHosted_TwoGigabyteFile_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
EmailVerified = true,
|
||||
Premium = false,
|
||||
};
|
||||
|
||||
send.UserId = user.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.SelfHosted = false;
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 2 * UserTests.Multiplier)
|
||||
);
|
||||
|
||||
Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var org = new Organization
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
MaxStorageGb = null,
|
||||
};
|
||||
|
||||
send.UserId = null;
|
||||
send.OrganizationId = org.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(org.Id)
|
||||
.Returns(org);
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 1)
|
||||
);
|
||||
|
||||
Assert.Contains("organization cannot use file sends", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_TwoGBFile_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var org = new Organization
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
MaxStorageGb = null,
|
||||
};
|
||||
|
||||
send.UserId = null;
|
||||
send.OrganizationId = org.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(org.Id)
|
||||
.Returns(org);
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 1)
|
||||
);
|
||||
|
||||
Assert.Contains("organization cannot use file sends", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsOneGB_TwoGBFile_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var org = new Organization
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
MaxStorageGb = 1,
|
||||
};
|
||||
|
||||
send.UserId = null;
|
||||
send.OrganizationId = org.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(org.Id)
|
||||
.Returns(org);
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, null, 2 * UserTests.Multiplier)
|
||||
);
|
||||
|
||||
Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_HasEnoughStorage_Success(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
EmailVerified = true,
|
||||
MaxStorageGb = 10,
|
||||
};
|
||||
|
||||
var data = new SendFileData
|
||||
{
|
||||
|
||||
};
|
||||
|
||||
send.UserId = user.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
var testUrl = "https://test.com/";
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ISendFileStorageService>()
|
||||
.GetSendFileUploadUrlAsync(send, Arg.Any<string>())
|
||||
.Returns(testUrl);
|
||||
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
var url = await sutProvider.Sut.SaveFileSendAsync(send, data, 1 * UserTests.Multiplier);
|
||||
|
||||
Assert.Equal(testUrl, url);
|
||||
Assert.True(send.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
|
||||
|
||||
await sutProvider.GetDependency<ISendFileStorageService>()
|
||||
.Received(1)
|
||||
.GetSendFileUploadUrlAsync(send, Arg.Any<string>());
|
||||
|
||||
await sutProvider.GetDependency<ISendRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(send);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncSendUpdateAsync(send);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveFileSendAsync_HasEnoughStorage_SendFileThrows_CleansUp(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
EmailVerified = true,
|
||||
MaxStorageGb = 10,
|
||||
};
|
||||
|
||||
var data = new SendFileData
|
||||
{
|
||||
|
||||
};
|
||||
|
||||
send.UserId = user.Id;
|
||||
send.Type = SendType.File;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ISendFileStorageService>()
|
||||
.GetSendFileUploadUrlAsync(send, Arg.Any<string>())
|
||||
.Returns<string>(callInfo => throw new Exception("Problem"));
|
||||
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<Exception>(() =>
|
||||
sutProvider.Sut.SaveFileSendAsync(send, data, 1 * UserTests.Multiplier)
|
||||
);
|
||||
|
||||
Assert.True(send.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
|
||||
Assert.Equal("Problem", exception.Message);
|
||||
|
||||
await sutProvider.GetDependency<ISendFileStorageService>()
|
||||
.Received(1)
|
||||
.GetSendFileUploadUrlAsync(send, Arg.Any<string>());
|
||||
|
||||
await sutProvider.GetDependency<ISendRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(send);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncSendUpdateAsync(send);
|
||||
|
||||
await sutProvider.GetDependency<ISendFileStorageService>()
|
||||
.Received(1)
|
||||
.DeleteFileAsync(send, Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateFileToExistingSendAsync_SendNull_ThrowsBadRequest(SutProvider<SendService> sutProvider)
|
||||
{
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(), null)
|
||||
);
|
||||
|
||||
Assert.Contains("does not have file data", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateFileToExistingSendAsync_SendDataNull_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
send.Data = null;
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(), send)
|
||||
);
|
||||
|
||||
Assert.Contains("does not have file data", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateFileToExistingSendAsync_NotFileType_ThrowsBadRequest(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(), send)
|
||||
);
|
||||
|
||||
Assert.Contains("not a file type send", badRequest.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateFileToExistingSendAsync_Success(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var fileContents = "Test file content";
|
||||
|
||||
var sendFileData = new SendFileData
|
||||
{
|
||||
Id = "TEST",
|
||||
Size = fileContents.Length,
|
||||
Validated = false,
|
||||
};
|
||||
|
||||
send.Type = SendType.File;
|
||||
send.Data = JsonSerializer.Serialize(sendFileData);
|
||||
|
||||
sutProvider.GetDependency<ISendFileStorageService>()
|
||||
.ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, Arg.Any<long>())
|
||||
.Returns((true, sendFileData.Size));
|
||||
|
||||
await sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(Encoding.UTF8.GetBytes(fileContents)), send);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateFileToExistingSendAsync_InvalidSize(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var fileContents = "Test file content";
|
||||
|
||||
var sendFileData = new SendFileData
|
||||
{
|
||||
Id = "TEST",
|
||||
Size = fileContents.Length,
|
||||
};
|
||||
|
||||
send.Type = SendType.File;
|
||||
send.Data = JsonSerializer.Serialize(sendFileData);
|
||||
|
||||
sutProvider.GetDependency<ISendFileStorageService>()
|
||||
.ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, Arg.Any<long>())
|
||||
.Returns((false, sendFileData.Size));
|
||||
|
||||
var badRequest = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(Encoding.UTF8.GetBytes(fileContents)), send)
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SendCanBeAccessed_Success(SutProvider<SendService> sutProvider, Send send)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
send.MaxAccessCount = 10;
|
||||
send.AccessCount = 5;
|
||||
send.ExpirationDate = now.AddYears(1);
|
||||
send.DeletionDate = now.AddYears(1);
|
||||
send.Disabled = false;
|
||||
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>()
|
||||
.VerifyHashedPassword(Arg.Any<User>(), send.Password, "TEST")
|
||||
.Returns(PasswordVerificationResult.Success);
|
||||
|
||||
var (grant, passwordRequiredError, passwordInvalidError)
|
||||
= sutProvider.Sut.SendCanBeAccessed(send, "TEST");
|
||||
|
||||
Assert.True(grant);
|
||||
Assert.False(passwordRequiredError);
|
||||
Assert.False(passwordInvalidError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SendCanBeAccessed_NullMaxAccess_Success(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
send.MaxAccessCount = null;
|
||||
send.AccessCount = 5;
|
||||
send.ExpirationDate = now.AddYears(1);
|
||||
send.DeletionDate = now.AddYears(1);
|
||||
send.Disabled = false;
|
||||
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>()
|
||||
.VerifyHashedPassword(Arg.Any<User>(), send.Password, "TEST")
|
||||
.Returns(PasswordVerificationResult.Success);
|
||||
|
||||
var (grant, passwordRequiredError, passwordInvalidError)
|
||||
= sutProvider.Sut.SendCanBeAccessed(send, "TEST");
|
||||
|
||||
Assert.True(grant);
|
||||
Assert.False(passwordRequiredError);
|
||||
Assert.False(passwordInvalidError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SendCanBeAccessed_NullSend_DoesNotGrantAccess(SutProvider<SendService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>()
|
||||
.VerifyHashedPassword(Arg.Any<User>(), "TEST", "TEST")
|
||||
.Returns(PasswordVerificationResult.Success);
|
||||
|
||||
var (grant, passwordRequiredError, passwordInvalidError)
|
||||
= sutProvider.Sut.SendCanBeAccessed(null, "TEST");
|
||||
|
||||
Assert.False(grant);
|
||||
Assert.False(passwordRequiredError);
|
||||
Assert.False(passwordInvalidError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SendCanBeAccessed_NullPassword_PasswordRequiredErrorReturnsTrue(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
send.MaxAccessCount = null;
|
||||
send.AccessCount = 5;
|
||||
send.ExpirationDate = now.AddYears(1);
|
||||
send.DeletionDate = now.AddYears(1);
|
||||
send.Disabled = false;
|
||||
send.Password = "HASH";
|
||||
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>()
|
||||
.VerifyHashedPassword(Arg.Any<User>(), "TEST", "TEST")
|
||||
.Returns(PasswordVerificationResult.Success);
|
||||
|
||||
var (grant, passwordRequiredError, passwordInvalidError)
|
||||
= sutProvider.Sut.SendCanBeAccessed(send, null);
|
||||
|
||||
Assert.False(grant);
|
||||
Assert.True(passwordRequiredError);
|
||||
Assert.False(passwordInvalidError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SendCanBeAccessed_RehashNeeded_RehashesPassword(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
send.MaxAccessCount = null;
|
||||
send.AccessCount = 5;
|
||||
send.ExpirationDate = now.AddYears(1);
|
||||
send.DeletionDate = now.AddYears(1);
|
||||
send.Disabled = false;
|
||||
send.Password = "TEST";
|
||||
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>()
|
||||
.VerifyHashedPassword(Arg.Any<User>(), "TEST", "TEST")
|
||||
.Returns(PasswordVerificationResult.SuccessRehashNeeded);
|
||||
|
||||
var (grant, passwordRequiredError, passwordInvalidError)
|
||||
= sutProvider.Sut.SendCanBeAccessed(send, "TEST");
|
||||
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>()
|
||||
.Received(1)
|
||||
.HashPassword(Arg.Any<User>(), "TEST");
|
||||
|
||||
Assert.True(grant);
|
||||
Assert.False(passwordRequiredError);
|
||||
Assert.False(passwordInvalidError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SendCanBeAccessed_VerifyFailed_PasswordInvalidReturnsTrue(SutProvider<SendService> sutProvider,
|
||||
Send send)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
send.MaxAccessCount = null;
|
||||
send.AccessCount = 5;
|
||||
send.ExpirationDate = now.AddYears(1);
|
||||
send.DeletionDate = now.AddYears(1);
|
||||
send.Disabled = false;
|
||||
send.Password = "TEST";
|
||||
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>()
|
||||
.VerifyHashedPassword(Arg.Any<User>(), "TEST", "TEST")
|
||||
.Returns(PasswordVerificationResult.Failed);
|
||||
|
||||
var (grant, passwordRequiredError, passwordInvalidError)
|
||||
= sutProvider.Sut.SendCanBeAccessed(send, "TEST");
|
||||
|
||||
Assert.False(grant);
|
||||
Assert.False(passwordRequiredError);
|
||||
Assert.True(passwordInvalidError);
|
||||
}
|
||||
}
|
@ -28,4 +28,31 @@ public class StaticStoreTests
|
||||
Assert.NotNull(plan);
|
||||
Assert.Equal(planType, plan.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticStore_GlobalEquivalentDomains_OnlyAsciiAllowed()
|
||||
{
|
||||
// Ref: https://daniel.haxx.se/blog/2025/05/16/detecting-malicious-unicode/
|
||||
// URLs can contain unicode characters that to a computer would point to completely seperate domains but to the
|
||||
// naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a
|
||||
// URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a
|
||||
// url update that could be missed in code review and then if they got a user to that URL Bitwarden could
|
||||
// consider it equivalent with a cipher in the users vault and offer autofill when we should not.
|
||||
// GitHub does now show a warning on non-ascii characters but it could still be missed.
|
||||
// https://github.blog/changelog/2025-05-01-github-now-provides-a-warning-about-hidden-unicode-text/
|
||||
|
||||
// To defend against this:
|
||||
// Loop through all equivalent domains and fail if any contain a non-ascii character
|
||||
// non-ascii character can make a valid URL so it's possible that in the future we have a domain
|
||||
// we want to allow list, that should be done through `continue`ing in the below foreach loop
|
||||
// only if the domain strictly equals (do NOT use InvariantCulture comparison) the one added to our allow list.
|
||||
foreach (var domain in StaticStore.GlobalDomains.SelectMany(p => p.Value))
|
||||
{
|
||||
for (var i = 0; i < domain.Length; i++)
|
||||
{
|
||||
var character = domain[i];
|
||||
Assert.True(char.IsAscii(character), $"Domain: {domain} contains non-ASCII character: '{character}' at index: {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.IntegrationTestCommon;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Events.IntegrationTest;
|
||||
@ -13,15 +13,18 @@ namespace Bit.Events.IntegrationTest;
|
||||
public class EventsApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
{
|
||||
private readonly IdentityApplicationFactory _identityApplicationFactory;
|
||||
private const string _connectionString = "DataSource=:memory:";
|
||||
|
||||
public EventsApplicationFactory()
|
||||
public EventsApplicationFactory() : this(new SqliteTestDatabase())
|
||||
{
|
||||
SqliteConnection = new SqliteConnection(_connectionString);
|
||||
SqliteConnection.Open();
|
||||
}
|
||||
|
||||
protected EventsApplicationFactory(ITestDatabase db)
|
||||
{
|
||||
TestDatabase = db;
|
||||
|
||||
_identityApplicationFactory = new IdentityApplicationFactory();
|
||||
_identityApplicationFactory.SqliteConnection = SqliteConnection;
|
||||
_identityApplicationFactory.TestDatabase = TestDatabase;
|
||||
_identityApplicationFactory.ManagesDatabase = false;
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
@ -42,6 +45,10 @@ public class EventsApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
/// </summary>
|
||||
public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash")
|
||||
{
|
||||
// This might be the first action in a test and since it forwards to the Identity server, we need to ensure that
|
||||
// this server is initialized since it's responsible for seeding the database.
|
||||
Assert.NotNull(Services);
|
||||
|
||||
await _identityApplicationFactory.RegisterNewIdentityFactoryUserAsync(
|
||||
new RegisterFinishRequestModel
|
||||
{
|
||||
@ -59,10 +66,4 @@ public class EventsApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
|
||||
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
SqliteConnection!.Dispose();
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.Auth.AutoFixture;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@ -57,8 +56,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||
|
||||
var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash,
|
||||
context => context.SetAuthEmail(user.Email));
|
||||
var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash);
|
||||
|
||||
using var body = await AssertDefaultTokenBodyAsync(context);
|
||||
var root = body.RootElement;
|
||||
@ -72,71 +70,6 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
AssertUserDecryptionOptions(root);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||
public async Task TokenEndpoint_GrantTypePassword_NoAuthEmailHeader_Fails(
|
||||
RegisterFinishRequestModel requestModel)
|
||||
{
|
||||
requestModel.Email = "test+noauthemailheader@email.com";
|
||||
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||
|
||||
var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, null);
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = body.RootElement;
|
||||
|
||||
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
|
||||
Assert.Equal("invalid_grant", error);
|
||||
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||
public async Task TokenEndpoint_GrantTypePassword_InvalidBase64AuthEmailHeader_Fails(
|
||||
RegisterFinishRequestModel requestModel)
|
||||
{
|
||||
requestModel.Email = "test+badauthheader@email.com";
|
||||
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||
|
||||
var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash,
|
||||
context => context.Request.Headers.Append("Auth-Email", "bad_value"));
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = body.RootElement;
|
||||
|
||||
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
|
||||
Assert.Equal("invalid_grant", error);
|
||||
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||
public async Task TokenEndpoint_GrantTypePassword_WrongAuthEmailHeader_Fails(
|
||||
RegisterFinishRequestModel requestModel)
|
||||
{
|
||||
requestModel.Email = "test+badauthheader@email.com";
|
||||
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||
|
||||
var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash,
|
||||
context => context.SetAuthEmail("bad_value"));
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = body.RootElement;
|
||||
|
||||
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
|
||||
Assert.Equal("invalid_grant", error);
|
||||
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
|
||||
}
|
||||
|
||||
[Theory, RegisterFinishRequestModelCustomize]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
@ -157,8 +90,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
await CreateOrganizationWithSsoPolicyAsync(localFactory,
|
||||
organizationId, user.Email, organizationUserType, ssoPolicyEnabled: false);
|
||||
|
||||
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash,
|
||||
context => context.SetAuthEmail(user.Email));
|
||||
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash);
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
}
|
||||
@ -184,8 +116,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
await CreateOrganizationWithSsoPolicyAsync(
|
||||
localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: false);
|
||||
|
||||
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash,
|
||||
context => context.SetAuthEmail(user.Email));
|
||||
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash);
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
}
|
||||
@ -209,8 +140,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
|
||||
await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true);
|
||||
|
||||
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash,
|
||||
context => context.SetAuthEmail(user.Email));
|
||||
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash);
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
await AssertRequiredSsoAuthenticationResponseAsync(context);
|
||||
@ -234,8 +164,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
|
||||
await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true);
|
||||
|
||||
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash,
|
||||
context => context.SetAuthEmail(user.Email));
|
||||
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash);
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
}
|
||||
@ -258,8 +187,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
|
||||
await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true);
|
||||
|
||||
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash,
|
||||
context => context.SetAuthEmail(user.Email));
|
||||
var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash);
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
await AssertRequiredSsoAuthenticationResponseAsync(context);
|
||||
@ -310,7 +238,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||
public async Task TokenEndpoint_GrantTypeClientCredentials_AsLegacyUser_NotOnWebClient_Fails(
|
||||
public async Task TokenEndpoint_GrantTypeClientCredentials_AsLegacyUser_Fails(
|
||||
RegisterFinishRequestModel model,
|
||||
string deviceId)
|
||||
{
|
||||
@ -342,14 +270,14 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
{ "grant_type", "password" },
|
||||
{ "username", model.Email },
|
||||
{ "password", model.MasterPasswordHash },
|
||||
}), context => context.SetAuthEmail(model.Email));
|
||||
}));
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
|
||||
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "ErrorModel", JsonValueKind.Object);
|
||||
var message = AssertHelper.AssertJsonProperty(error, "Message", JsonValueKind.String).GetString();
|
||||
Assert.StartsWith("Encryption key migration is required.", message);
|
||||
Assert.StartsWith("Legacy encryption without a userkey is no longer supported.", message);
|
||||
}
|
||||
|
||||
|
||||
@ -554,12 +482,12 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
{ "grant_type", "password" },
|
||||
{ "username", user.Email},
|
||||
{ "password", "master_password_hash" },
|
||||
}), context => context.SetAuthEmail(user.Email).SetIp("1.1.1.2"));
|
||||
}), context => context.SetIp("1.1.1.2"));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpContext> PostLoginAsync(
|
||||
TestServer server, User user, string MasterPasswordHash, Action<HttpContext> extraConfiguration)
|
||||
TestServer server, User user, string MasterPasswordHash)
|
||||
{
|
||||
return await server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
@ -571,7 +499,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
{ "grant_type", "password" },
|
||||
{ "username", user.Email },
|
||||
{ "password", MasterPasswordHash },
|
||||
}), extraConfiguration);
|
||||
}));
|
||||
}
|
||||
|
||||
private async Task CreateOrganizationWithSsoPolicyAsync(
|
||||
|
@ -143,7 +143,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
{ "grant_type", "password" },
|
||||
{ "username", _testEmail },
|
||||
{ "password", _testPassword },
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail)));
|
||||
}));
|
||||
|
||||
// Assert
|
||||
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
@ -263,7 +263,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
{ "code", "test_code" },
|
||||
{ "code_verifier", challenge },
|
||||
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail)));
|
||||
}));
|
||||
|
||||
// Assert
|
||||
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
@ -307,7 +307,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
{ "code", "test_code" },
|
||||
{ "code_verifier", challenge },
|
||||
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail)));
|
||||
}));
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, failedTokenContext.Response.StatusCode);
|
||||
Assert.NotNull(emailToken);
|
||||
@ -326,7 +326,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
{ "code", "test_code" },
|
||||
{ "code_verifier", challenge },
|
||||
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail)));
|
||||
}));
|
||||
|
||||
|
||||
// Assert
|
||||
@ -363,7 +363,7 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
|
||||
{ "code", "test_code" },
|
||||
{ "code_verifier", challenge },
|
||||
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail)));
|
||||
}));
|
||||
|
||||
// Assert
|
||||
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
|
@ -29,8 +29,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
||||
|
||||
// Act
|
||||
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||
GetFormUrlEncodedContent(),
|
||||
context => context.SetAuthEmail(DefaultUsername));
|
||||
GetFormUrlEncodedContent());
|
||||
|
||||
// Assert
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
@ -40,27 +39,6 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
||||
Assert.NotNull(token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_AuthEmailHeaderInvalid_InvalidGrantResponse()
|
||||
{
|
||||
// Arrange
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
await EnsureUserCreatedAsync(localFactory);
|
||||
|
||||
// Act
|
||||
var context = await localFactory.Server.PostAsync(
|
||||
"/connect/token",
|
||||
GetFormUrlEncodedContent()
|
||||
);
|
||||
|
||||
// Assert
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = body.RootElement;
|
||||
|
||||
var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString();
|
||||
Assert.Equal("Auth-Email header invalid.", error);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_UserNull_Failure(string username)
|
||||
{
|
||||
@ -68,8 +46,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
// Act
|
||||
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||
GetFormUrlEncodedContent(username: username),
|
||||
context => context.SetAuthEmail(username));
|
||||
GetFormUrlEncodedContent(username: username));
|
||||
|
||||
// Assert
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
@ -106,8 +83,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
||||
|
||||
// Act
|
||||
var context = await localFactory.Server.PostAsync("/connect/token",
|
||||
GetFormUrlEncodedContent(password: badPassword),
|
||||
context => context.SetAuthEmail(DefaultUsername));
|
||||
GetFormUrlEncodedContent(password: badPassword));
|
||||
|
||||
// Assert
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
@ -155,7 +131,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
||||
{ "username", DefaultUsername },
|
||||
{ "password", DefaultPassword },
|
||||
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||
}), context => context.SetAuthEmail(DefaultUsername));
|
||||
}));
|
||||
|
||||
// Assert
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
@ -197,7 +173,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
||||
{ "username", DefaultUsername },
|
||||
{ "password", DefaultPassword },
|
||||
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||
}), context => context.SetAuthEmail(DefaultUsername));
|
||||
}));
|
||||
|
||||
// Assert
|
||||
|
||||
|
@ -14,9 +14,6 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Identity.Controllers;
|
||||
using Bit.Identity.Models.Request.Accounts;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -40,7 +37,6 @@ public class AccountsControllerTests : IDisposable
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
@ -55,7 +51,6 @@ public class AccountsControllerTests : IDisposable
|
||||
_assertionOptionsDataProtector = Substitute.For<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>();
|
||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = Substitute.For<IGetWebAuthnLoginCredentialAssertionOptionsCommand>();
|
||||
_sendVerificationEmailForRegistrationCommand = Substitute.For<ISendVerificationEmailForRegistrationCommand>();
|
||||
_referenceEventService = Substitute.For<IReferenceEventService>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_registrationEmailVerificationTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>();
|
||||
_globalSettings = Substitute.For<GlobalSettings>();
|
||||
@ -68,7 +63,6 @@ public class AccountsControllerTests : IDisposable
|
||||
_assertionOptionsDataProtector,
|
||||
_getWebAuthnLoginCredentialAssertionOptionsCommand,
|
||||
_sendVerificationEmailForRegistrationCommand,
|
||||
_referenceEventService,
|
||||
_featureService,
|
||||
_registrationEmailVerificationTokenDataFactory,
|
||||
_globalSettings
|
||||
@ -163,8 +157,6 @@ public class AccountsControllerTests : IDisposable
|
||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
||||
Assert.Equal(200, okResult.StatusCode);
|
||||
Assert.Equal(token, okResult.Value);
|
||||
|
||||
await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(e => e.Type == ReferenceEventType.SignupEmailSubmit));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -187,7 +179,6 @@ public class AccountsControllerTests : IDisposable
|
||||
// Assert
|
||||
var noContentResult = Assert.IsType<NoContentResult>(result);
|
||||
Assert.Equal(204, noContentResult.StatusCode);
|
||||
await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(e => e.Type == ReferenceEventType.SignupEmailSubmit));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -404,12 +395,6 @@ public class AccountsControllerTests : IDisposable
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkResult>(result);
|
||||
Assert.Equal(200, okResult.StatusCode);
|
||||
|
||||
await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(e =>
|
||||
e.Type == ReferenceEventType.SignupEmailClicked
|
||||
&& e.EmailVerificationTokenValid == true
|
||||
&& e.UserAlreadyExists == false
|
||||
));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -435,12 +420,6 @@ public class AccountsControllerTests : IDisposable
|
||||
|
||||
// Act & assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostRegisterVerificationEmailClicked(requestModel));
|
||||
|
||||
await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(e =>
|
||||
e.Type == ReferenceEventType.SignupEmailClicked
|
||||
&& e.EmailVerificationTokenValid == false
|
||||
&& e.UserAlreadyExists == false
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@ -467,12 +446,6 @@ public class AccountsControllerTests : IDisposable
|
||||
|
||||
// Act & assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostRegisterVerificationEmailClicked(requestModel));
|
||||
|
||||
await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(e =>
|
||||
e.Type == ReferenceEventType.SignupEmailClicked
|
||||
&& e.EmailVerificationTokenValid == true
|
||||
&& e.UserAlreadyExists == true
|
||||
));
|
||||
}
|
||||
|
||||
private void SetDefaultKdfHmacKey(byte[]? newKey)
|
||||
|
@ -373,8 +373,7 @@ public class BaseRequestValidatorTests
|
||||
// Assert
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
var expectedMessage = $"Encryption key migration is required. Please log in to the web " +
|
||||
$"vault at {_globalSettings.BaseServiceUri.VaultWithHash}";
|
||||
var expectedMessage = "Legacy encryption without a userkey is no longer supported. To recover your account, please contact support";
|
||||
Assert.Equal(expectedMessage, errorResponse.Message);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,75 @@
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Identity.IdentityServer.ClientProviders;
|
||||
using IdentityModel;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer.ClientProviders;
|
||||
|
||||
public class InstallationClientProviderTests
|
||||
{
|
||||
|
||||
private readonly IInstallationRepository _installationRepository;
|
||||
private readonly InstallationClientProvider _sut;
|
||||
|
||||
public InstallationClientProviderTests()
|
||||
{
|
||||
_installationRepository = Substitute.For<IInstallationRepository>();
|
||||
|
||||
_sut = new InstallationClientProvider(_installationRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_NonGuidIdentifier_ReturnsNull()
|
||||
{
|
||||
var installationClient = await _sut.GetAsync("non-guid");
|
||||
|
||||
Assert.Null(installationClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_NonExistingInstallationGuid_ReturnsNull()
|
||||
{
|
||||
var installationClient = await _sut.GetAsync(Guid.NewGuid().ToString());
|
||||
|
||||
Assert.Null(installationClient);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task GetAsync_ExistingClient_ReturnsClientRespectingEnabledStatus(bool enabled)
|
||||
{
|
||||
var installationId = Guid.NewGuid();
|
||||
|
||||
_installationRepository
|
||||
.GetByIdAsync(installationId)
|
||||
.Returns(new Installation
|
||||
{
|
||||
Id = installationId,
|
||||
Key = "some-key",
|
||||
Email = "some-email",
|
||||
Enabled = enabled,
|
||||
});
|
||||
|
||||
var installationClient = await _sut.GetAsync(installationId.ToString());
|
||||
|
||||
Assert.NotNull(installationClient);
|
||||
Assert.Equal($"installation.{installationId}", installationClient.ClientId);
|
||||
Assert.True(installationClient.RequireClientSecret);
|
||||
// The usage of this secret is tested in integration tests
|
||||
Assert.Single(installationClient.ClientSecrets);
|
||||
Assert.Collection(
|
||||
installationClient.AllowedScopes,
|
||||
s => Assert.Equal(ApiScopes.ApiPush, s),
|
||||
s => Assert.Equal(ApiScopes.ApiLicensing, s),
|
||||
s => Assert.Equal(ApiScopes.ApiInstallation, s)
|
||||
);
|
||||
Assert.Equal(enabled, installationClient.Enabled);
|
||||
Assert.Equal(TimeSpan.FromDays(1).TotalSeconds, installationClient.AccessTokenLifetime);
|
||||
var claim = Assert.Single(installationClient.Claims);
|
||||
Assert.Equal(JwtClaimTypes.Subject, claim.Type);
|
||||
Assert.Equal(installationId.ToString(), claim.Value);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Identity.IdentityServer.ClientProviders;
|
||||
using IdentityModel;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer.ClientProviders;
|
||||
|
||||
public class InternalClientProviderTests
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
private readonly InternalClientProvider _sut;
|
||||
|
||||
public InternalClientProviderTests()
|
||||
{
|
||||
_globalSettings = new GlobalSettings
|
||||
{
|
||||
SelfHosted = true,
|
||||
};
|
||||
_sut = new InternalClientProvider(_globalSettings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_ReturnsInternalClient()
|
||||
{
|
||||
var internalClient = await _sut.GetAsync("blah");
|
||||
|
||||
Assert.NotNull(internalClient);
|
||||
Assert.Equal($"internal.blah", internalClient.ClientId);
|
||||
Assert.True(internalClient.RequireClientSecret);
|
||||
var secret = Assert.Single(internalClient.ClientSecrets);
|
||||
Assert.NotNull(secret);
|
||||
Assert.NotNull(secret.Value);
|
||||
var scope = Assert.Single(internalClient.AllowedScopes);
|
||||
Assert.Equal(ApiScopes.Internal, scope);
|
||||
Assert.Equal(TimeSpan.FromDays(1).TotalSeconds, internalClient.AccessTokenLifetime);
|
||||
Assert.True(internalClient.Enabled);
|
||||
var claim = Assert.Single(internalClient.Claims);
|
||||
Assert.Equal(JwtClaimTypes.Subject, claim.Type);
|
||||
Assert.Equal("blah", claim.Value);
|
||||
}
|
||||
}
|
148
test/Identity.Test/IdentityServer/DynamicClientStoreTests.cs
Normal file
148
test/Identity.Test/IdentityServer/DynamicClientStoreTests.cs
Normal file
@ -0,0 +1,148 @@
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer;
|
||||
|
||||
public class DynamicClientStoreTests
|
||||
{
|
||||
private readonly IServiceCollection _services;
|
||||
private readonly IClientProvider _apiKeyProvider;
|
||||
|
||||
private readonly Func<DynamicClientStore> _sutCreator;
|
||||
|
||||
public DynamicClientStoreTests()
|
||||
{
|
||||
_services = new ServiceCollection();
|
||||
_apiKeyProvider = Substitute.For<IClientProvider>();
|
||||
|
||||
_sutCreator = () => new DynamicClientStore(
|
||||
_services.BuildServiceProvider(),
|
||||
_apiKeyProvider,
|
||||
new StaticClientStore(new Core.Settings.GlobalSettings())
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("mobile")]
|
||||
[InlineData("web")]
|
||||
[InlineData("browser")]
|
||||
[InlineData("desktop")]
|
||||
[InlineData("cli")]
|
||||
[InlineData("connector")]
|
||||
public async Task FindClientByIdAsync_StaticClients_Works(string staticClientId)
|
||||
{
|
||||
var sut = _sutCreator();
|
||||
|
||||
var client = await sut.FindClientByIdAsync(staticClientId);
|
||||
|
||||
Assert.NotNull(client);
|
||||
Assert.Equal(staticClientId, client.ClientId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindClientByIdAsync_SplitName_NoService_ReturnsNull()
|
||||
{
|
||||
_services.AddClientProvider<FakeClientProvider>("my-provider");
|
||||
|
||||
var sut = _sutCreator();
|
||||
|
||||
var client = await sut.FindClientByIdAsync("blah.something");
|
||||
|
||||
Assert.Null(client);
|
||||
|
||||
await _apiKeyProvider
|
||||
.Received(0)
|
||||
.GetAsync(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task FindClientByIdAsync_SplitName_HasService_ReturnsValueFromService(bool returnNull)
|
||||
{
|
||||
var fakeProvider = Substitute.For<IClientProvider>();
|
||||
|
||||
fakeProvider
|
||||
.GetAsync("something")
|
||||
.Returns(returnNull ? null : new Client { ClientId = "fake" });
|
||||
|
||||
_services.AddKeyedSingleton("my-provider", fakeProvider);
|
||||
|
||||
var sut = _sutCreator();
|
||||
|
||||
var client = await sut.FindClientByIdAsync("my-provider.something");
|
||||
|
||||
if (returnNull)
|
||||
{
|
||||
Assert.Null(client);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.NotNull(client);
|
||||
}
|
||||
|
||||
await fakeProvider
|
||||
.Received(1)
|
||||
.GetAsync("something");
|
||||
|
||||
await _apiKeyProvider
|
||||
.Received(0)
|
||||
.GetAsync(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task FindClientByIdAsync_RandomString_NotSplit_TriesApiKey(bool returnsNull)
|
||||
{
|
||||
_apiKeyProvider
|
||||
.GetAsync("random-string")
|
||||
.Returns(returnsNull ? null : new Client { ClientId = "test" });
|
||||
|
||||
var sut = _sutCreator();
|
||||
|
||||
var client = await sut.FindClientByIdAsync("random-string");
|
||||
|
||||
if (returnsNull)
|
||||
{
|
||||
Assert.Null(client);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.NotNull(client);
|
||||
}
|
||||
|
||||
await _apiKeyProvider
|
||||
.Received(1)
|
||||
.GetAsync("random-string");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("id.")]
|
||||
[InlineData("id. ")]
|
||||
public async Task FindClientByIdAsync_InvalidIdentifierValue_ReturnsNull(string clientId)
|
||||
{
|
||||
var sut = _sutCreator();
|
||||
|
||||
var client = await sut.FindClientByIdAsync(clientId);
|
||||
Assert.Null(client);
|
||||
}
|
||||
|
||||
private class FakeClientProvider : IClientProvider
|
||||
{
|
||||
public FakeClientProvider()
|
||||
{
|
||||
Fake = Substitute.For<IClientProvider>();
|
||||
}
|
||||
|
||||
public IClientProvider Fake { get; }
|
||||
|
||||
public Task<Client?> GetAsync(string identifier)
|
||||
{
|
||||
return Fake.GetAsync(identifier);
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ public class UserDecryptionOptionsBuilderTests
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ILoginApprovingClientTypes _loginApprovingClientTypes;
|
||||
private readonly UserDecryptionOptionsBuilder _builder;
|
||||
|
||||
public UserDecryptionOptionsBuilderTests()
|
||||
@ -25,7 +26,8 @@ public class UserDecryptionOptionsBuilderTests
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
_deviceRepository = Substitute.For<IDeviceRepository>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository);
|
||||
_loginApprovingClientTypes = Substitute.For<ILoginApprovingClientTypes>();
|
||||
_builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -102,12 +104,39 @@ public class UserDecryptionOptionsBuilderTests
|
||||
Assert.Equal(device.EncryptedUserKey, result.TrustedDeviceOption?.EncryptedUserKey);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue(SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice)
|
||||
[Theory]
|
||||
// Desktop
|
||||
[BitAutoData(DeviceType.LinuxDesktop)]
|
||||
[BitAutoData(DeviceType.MacOsDesktop)]
|
||||
[BitAutoData(DeviceType.WindowsDesktop)]
|
||||
[BitAutoData(DeviceType.UWP)]
|
||||
// Mobile
|
||||
[BitAutoData(DeviceType.Android)]
|
||||
[BitAutoData(DeviceType.iOS)]
|
||||
[BitAutoData(DeviceType.AndroidAmazon)]
|
||||
// Web
|
||||
[BitAutoData(DeviceType.ChromeBrowser)]
|
||||
[BitAutoData(DeviceType.FirefoxBrowser)]
|
||||
[BitAutoData(DeviceType.OperaBrowser)]
|
||||
[BitAutoData(DeviceType.EdgeBrowser)]
|
||||
[BitAutoData(DeviceType.IEBrowser)]
|
||||
[BitAutoData(DeviceType.SafariBrowser)]
|
||||
[BitAutoData(DeviceType.VivaldiBrowser)]
|
||||
[BitAutoData(DeviceType.UnknownBrowser)]
|
||||
public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue(
|
||||
DeviceType deviceType,
|
||||
SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice)
|
||||
{
|
||||
_loginApprovingClientTypes.TypesThatCanApprove.Returns(new List<ClientType>
|
||||
{
|
||||
ClientType.Desktop,
|
||||
ClientType.Mobile,
|
||||
ClientType.Web,
|
||||
});
|
||||
|
||||
configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
ssoConfig.Data = configurationData.Serialize();
|
||||
approvingDevice.Type = LoginApprovingDeviceTypes.Types.First();
|
||||
approvingDevice.Type = deviceType;
|
||||
_deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice });
|
||||
|
||||
var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync();
|
||||
@ -115,6 +144,80 @@ public class UserDecryptionOptionsBuilderTests
|
||||
Assert.True(result.TrustedDeviceOption?.HasLoginApprovingDevice);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// Desktop
|
||||
[BitAutoData(DeviceType.LinuxDesktop)]
|
||||
[BitAutoData(DeviceType.MacOsDesktop)]
|
||||
[BitAutoData(DeviceType.WindowsDesktop)]
|
||||
[BitAutoData(DeviceType.UWP)]
|
||||
// Mobile
|
||||
[BitAutoData(DeviceType.Android)]
|
||||
[BitAutoData(DeviceType.iOS)]
|
||||
[BitAutoData(DeviceType.AndroidAmazon)]
|
||||
// Web
|
||||
[BitAutoData(DeviceType.ChromeBrowser)]
|
||||
[BitAutoData(DeviceType.FirefoxBrowser)]
|
||||
[BitAutoData(DeviceType.OperaBrowser)]
|
||||
[BitAutoData(DeviceType.EdgeBrowser)]
|
||||
[BitAutoData(DeviceType.IEBrowser)]
|
||||
[BitAutoData(DeviceType.SafariBrowser)]
|
||||
[BitAutoData(DeviceType.VivaldiBrowser)]
|
||||
[BitAutoData(DeviceType.UnknownBrowser)]
|
||||
// Extension
|
||||
[BitAutoData(DeviceType.ChromeExtension)]
|
||||
[BitAutoData(DeviceType.FirefoxExtension)]
|
||||
[BitAutoData(DeviceType.OperaExtension)]
|
||||
[BitAutoData(DeviceType.EdgeExtension)]
|
||||
[BitAutoData(DeviceType.VivaldiExtension)]
|
||||
[BitAutoData(DeviceType.SafariExtension)]
|
||||
public async Task Build_WhenHasLoginApprovingDeviceFeatureFlag_ShouldApprovingDeviceTrue(
|
||||
DeviceType deviceType,
|
||||
SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice)
|
||||
{
|
||||
_loginApprovingClientTypes.TypesThatCanApprove.Returns(new List<ClientType>
|
||||
{
|
||||
ClientType.Desktop,
|
||||
ClientType.Mobile,
|
||||
ClientType.Web,
|
||||
ClientType.Browser,
|
||||
});
|
||||
|
||||
configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
ssoConfig.Data = configurationData.Serialize();
|
||||
approvingDevice.Type = deviceType;
|
||||
_deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice });
|
||||
|
||||
var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync();
|
||||
|
||||
Assert.True(result.TrustedDeviceOption?.HasLoginApprovingDevice);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// CLI
|
||||
[BitAutoData(DeviceType.WindowsCLI)]
|
||||
[BitAutoData(DeviceType.MacOsCLI)]
|
||||
[BitAutoData(DeviceType.LinuxCLI)]
|
||||
// Extension
|
||||
[BitAutoData(DeviceType.ChromeExtension)]
|
||||
[BitAutoData(DeviceType.FirefoxExtension)]
|
||||
[BitAutoData(DeviceType.OperaExtension)]
|
||||
[BitAutoData(DeviceType.EdgeExtension)]
|
||||
[BitAutoData(DeviceType.VivaldiExtension)]
|
||||
[BitAutoData(DeviceType.SafariExtension)]
|
||||
public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceFalse(
|
||||
DeviceType deviceType,
|
||||
SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice)
|
||||
{
|
||||
configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
ssoConfig.Data = configurationData.Serialize();
|
||||
approvingDevice.Type = deviceType;
|
||||
_deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice });
|
||||
|
||||
var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync();
|
||||
|
||||
Assert.False(result.TrustedDeviceOption?.HasLoginApprovingDevice);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Build_WhenManageResetPasswordPermissions_ShouldReturnHasManageResetPasswordPermissionTrue(
|
||||
SsoConfig ssoConfig,
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||
using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using Xunit;
|
||||
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Organization = Bit.Core.AdminConsole.Entities.Organization;
|
||||
@ -161,7 +162,7 @@ public class OrganizationRepositoryTests
|
||||
|
||||
[CiSkippedTheory, EfOrganizationUserAutoData]
|
||||
public async Task SearchUnassignedAsync_Works(OrganizationUser orgUser, User user, Organization org,
|
||||
List<EfRepo.OrganizationUserRepository> efOrgUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
||||
List<OrganizationUserRepository> efOrgUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo, SqlRepo.UserRepository sqlUserRepo)
|
||||
{
|
||||
orgUser.Type = OrganizationUserType.Owner;
|
||||
|
@ -24,7 +24,7 @@ public class OrganizationUserRepositoryTests
|
||||
{
|
||||
[CiSkippedTheory, EfOrganizationUserAutoData]
|
||||
public async Task CreateAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org,
|
||||
OrganizationUserCompare equalityComparer, List<EfRepo.OrganizationUserRepository> suts,
|
||||
OrganizationUserCompare equalityComparer, List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||
List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrgRepo)
|
||||
@ -67,7 +67,7 @@ public class OrganizationUserRepositoryTests
|
||||
User user,
|
||||
Organization org,
|
||||
OrganizationUserCompare equalityComparer,
|
||||
List<EfRepo.OrganizationUserRepository> suts,
|
||||
List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||
List<EfRepo.UserRepository> efUserRepos,
|
||||
List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo,
|
||||
@ -113,7 +113,7 @@ public class OrganizationUserRepositoryTests
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfOrganizationUserAutoData]
|
||||
public async Task DeleteAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, List<EfRepo.OrganizationUserRepository> suts,
|
||||
public async Task DeleteAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||
List<EfRepo.UserRepository> efUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrgRepo)
|
||||
@ -188,7 +188,7 @@ public class OrganizationUserRepositoryTests
|
||||
List<EfAdminConsoleRepo.PolicyRepository> efPolicyRepository,
|
||||
List<EfRepo.UserRepository> efUserRepository,
|
||||
List<EfRepo.OrganizationRepository> efOrganizationRepository,
|
||||
List<EfRepo.OrganizationUserRepository> suts,
|
||||
List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||
List<EfAdminConsoleRepo.ProviderRepository> efProviderRepository,
|
||||
List<EfAdminConsoleRepo.ProviderOrganizationRepository> efProviderOrganizationRepository,
|
||||
List<EfAdminConsoleRepo.ProviderUserRepository> efProviderUserRepository,
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Core.Test.AutoFixture.UserFixtures;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
@ -5,6 +5,7 @@ using Bit.Core.Test.AutoFixture.UserFixtures;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture.Relays;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
|
@ -9,6 +9,7 @@ using Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using LinqToDB;
|
||||
using Xunit;
|
||||
using EfAdminConsoleRepo = Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using EfVaultRepo = Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
||||
using SqlRepo = Bit.Infrastructure.Dapper.Repositories;
|
||||
@ -112,7 +113,7 @@ public class CipherRepositoryTests
|
||||
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
|
||||
public async Task CreateAsync_BumpsOrgUserAccountRevisionDates(Cipher cipher, List<User> users,
|
||||
List<OrganizationUser> orgUsers, Collection collection, Organization org, List<EfVaultRepo.CipherRepository> suts, List<EfRepo.UserRepository> efUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||
List<EfRepo.OrganizationUserRepository> efOrgUserRepos, List<EfRepo.CollectionRepository> efCollectionRepos)
|
||||
List<EfAdminConsoleRepo.OrganizationUserRepository> efOrgUserRepos, List<EfRepo.CollectionRepository> efCollectionRepos)
|
||||
{
|
||||
var savedCiphers = new List<Cipher>();
|
||||
foreach (var sut in suts)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
@ -26,15 +27,23 @@ public static class OrganizationTestHelpers
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Enterprise organization.
|
||||
/// </summary>
|
||||
public static Task<Organization> CreateTestOrganizationAsync(this IOrganizationRepository organizationRepository,
|
||||
string identifier = "test")
|
||||
=> organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = $"{identifier}-{Guid.NewGuid()}",
|
||||
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
|
||||
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
|
||||
Plan = "Enterprise (Annually)", // TODO: EF does not enforce this being NOT NULl
|
||||
PlanType = PlanType.EnterpriseAnnually
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Creates a confirmed Owner for the specified organization and user.
|
||||
/// Does not include any cryptographic material.
|
||||
/// </summary>
|
||||
public static Task<OrganizationUser> CreateTestOrganizationUserAsync(
|
||||
this IOrganizationUserRepository organizationUserRepository,
|
||||
Organization organization,
|
||||
@ -47,6 +56,17 @@ public static class OrganizationTestHelpers
|
||||
Type = OrganizationUserType.Owner
|
||||
});
|
||||
|
||||
public static Task<OrganizationUser> CreateTestOrganizationUserInviteAsync(
|
||||
this IOrganizationUserRepository organizationUserRepository,
|
||||
Organization organization)
|
||||
=> organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = null, // Invites are not linked to a UserId
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
Type = OrganizationUserType.Owner
|
||||
});
|
||||
|
||||
public static Task<Group> CreateTestGroupAsync(
|
||||
this IGroupRepository groupRepository,
|
||||
Organization organization,
|
||||
@ -54,4 +74,14 @@ public static class OrganizationTestHelpers
|
||||
=> groupRepository.CreateAsync(
|
||||
new Group { OrganizationId = organization.Id, Name = $"{identifier} {Guid.NewGuid()}" }
|
||||
);
|
||||
|
||||
public static Task<Collection> CreateTestCollectionAsync(
|
||||
this ICollectionRepository collectionRepository,
|
||||
Organization organization,
|
||||
string identifier = "test")
|
||||
=> collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Name = $"{identifier} {Guid.NewGuid()}"
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,105 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||
|
||||
public class CollectionRepositoryCreateTests
|
||||
{
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task CreateAsync_WithAccess_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var user1 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1);
|
||||
|
||||
var user2 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2);
|
||||
|
||||
var group1 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
var group2 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
// Act
|
||||
await collectionRepository.CreateAsync(collection,
|
||||
[
|
||||
new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false },
|
||||
]
|
||||
);
|
||||
|
||||
// Assert
|
||||
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||
|
||||
Assert.NotNull(actualCollection);
|
||||
Assert.Equal("Test Collection Name", actualCollection.Name);
|
||||
|
||||
var groups = actualAccess.Groups.ToArray();
|
||||
Assert.Equal(2, groups.Length);
|
||||
Assert.Single(groups, g => g.Id == group1.Id && g.Manage && g.HidePasswords && !g.ReadOnly);
|
||||
Assert.Single(groups, g => g.Id == group2.Id && !g.Manage && !g.HidePasswords && g.ReadOnly);
|
||||
|
||||
var users = actualAccess.Users.ToArray();
|
||||
Assert.Equal(2, users.Length);
|
||||
Assert.Single(users, u => u.Id == orgUser1.Id && u.Manage && !u.HidePasswords && u.ReadOnly);
|
||||
Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && u.HidePasswords && !u.ReadOnly);
|
||||
|
||||
// Clean up data
|
||||
await userRepository.DeleteAsync(user1);
|
||||
await userRepository.DeleteAsync(user2);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await groupRepository.DeleteManyAsync([group1.Id, group2.Id]);
|
||||
await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id]);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Makes sure that the sproc handles empty sets.
|
||||
/// </remarks>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task CreateAsync_WithNoAccess_Works(
|
||||
IOrganizationRepository organizationRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
// Act
|
||||
await collectionRepository.CreateAsync(collection, [], []);
|
||||
|
||||
// Assert
|
||||
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||
|
||||
Assert.NotNull(actualCollection);
|
||||
Assert.Equal("Test Collection Name", actualCollection.Name);
|
||||
|
||||
Assert.Empty(actualAccess.Groups);
|
||||
Assert.Empty(actualAccess.Users);
|
||||
|
||||
// Clean up
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||
|
||||
public class CollectionRepositoryReplaceTests
|
||||
{
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ReplaceAsync_WithAccess_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var user1 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1);
|
||||
|
||||
var user2 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2);
|
||||
|
||||
var user3 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser3 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user3);
|
||||
|
||||
var group1 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
var group2 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
var group3 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
await collectionRepository.CreateAsync(collection,
|
||||
[
|
||||
new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false },
|
||||
]
|
||||
);
|
||||
|
||||
// Act
|
||||
collection.Name = "Updated Collection Name";
|
||||
|
||||
await collectionRepository.ReplaceAsync(collection,
|
||||
[
|
||||
// Delete group1
|
||||
// Update group2:
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
// Add group3:
|
||||
new CollectionAccessSelection { Id = group3.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
// Delete orgUser1
|
||||
// Update orgUser2:
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = false, ReadOnly = true },
|
||||
// Add orgUser3:
|
||||
new CollectionAccessSelection { Id = orgUser3.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
]
|
||||
);
|
||||
|
||||
// Assert
|
||||
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||
|
||||
Assert.NotNull(actualCollection);
|
||||
Assert.Equal("Updated Collection Name", actualCollection.Name);
|
||||
|
||||
var groups = actualAccess.Groups.ToArray();
|
||||
Assert.Equal(2, groups.Length);
|
||||
Assert.Single(groups, g => g.Id == group2.Id && g.Manage && g.HidePasswords && !g.ReadOnly);
|
||||
Assert.Single(groups, g => g.Id == group3.Id && !g.Manage && !g.HidePasswords && g.ReadOnly);
|
||||
|
||||
var users = actualAccess.Users.ToArray();
|
||||
|
||||
Assert.Equal(2, users.Length);
|
||||
Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && !u.HidePasswords && u.ReadOnly);
|
||||
Assert.Single(users, u => u.Id == orgUser3.Id && u.Manage && !u.HidePasswords && u.ReadOnly);
|
||||
|
||||
// Clean up data
|
||||
await userRepository.DeleteAsync(user1);
|
||||
await userRepository.DeleteAsync(user2);
|
||||
await userRepository.DeleteAsync(user3);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Makes sure that the sproc handles empty sets.
|
||||
/// </remarks>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ReplaceAsync_WithNoAccess_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
|
||||
|
||||
var group = await groupRepository.CreateTestGroupAsync(organization);
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
await collectionRepository.CreateAsync(collection,
|
||||
[
|
||||
new CollectionAccessSelection { Id = group.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
],
|
||||
[
|
||||
new CollectionAccessSelection { Id = orgUser.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
]);
|
||||
|
||||
// Act
|
||||
collection.Name = "Updated Collection Name";
|
||||
|
||||
await collectionRepository.ReplaceAsync(collection, [], []);
|
||||
|
||||
// Assert
|
||||
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||
|
||||
Assert.NotNull(actualCollection);
|
||||
Assert.Equal("Updated Collection Name", actualCollection.Name);
|
||||
|
||||
Assert.Empty(actualAccess.Groups);
|
||||
Assert.Empty(actualAccess.Users);
|
||||
|
||||
// Clean up
|
||||
await userRepository.DeleteAsync(user);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.Repositories;
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||
|
||||
public class CollectionRepositoryTests
|
||||
{
|
||||
@ -463,147 +463,4 @@ public class CollectionRepositoryTests
|
||||
Assert.False(c3.Unmanaged);
|
||||
});
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ReplaceAsync_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
Plan = "Test Plan",
|
||||
BillingEmail = "billing@email.com"
|
||||
});
|
||||
|
||||
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
});
|
||||
|
||||
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
});
|
||||
|
||||
var orgUser3 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
});
|
||||
|
||||
var group1 = await groupRepository.CreateAsync(new Group
|
||||
{
|
||||
Name = "Test Group #1",
|
||||
OrganizationId = organization.Id,
|
||||
});
|
||||
|
||||
var group2 = await groupRepository.CreateAsync(new Group
|
||||
{
|
||||
Name = "Test Group #2",
|
||||
OrganizationId = organization.Id,
|
||||
});
|
||||
|
||||
var group3 = await groupRepository.CreateAsync(new Group
|
||||
{
|
||||
Name = "Test Group #3",
|
||||
OrganizationId = organization.Id,
|
||||
});
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
await collectionRepository.CreateAsync(collection,
|
||||
[
|
||||
new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false },
|
||||
]
|
||||
);
|
||||
|
||||
collection.Name = "Updated Collection Name";
|
||||
|
||||
await collectionRepository.ReplaceAsync(collection,
|
||||
[
|
||||
// Should delete group1
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
// Should add group3
|
||||
new CollectionAccessSelection { Id = group3.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
// Should delete orgUser1
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = false, ReadOnly = true },
|
||||
// Should add orgUser3
|
||||
new CollectionAccessSelection { Id = orgUser3.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
]
|
||||
);
|
||||
|
||||
// Assert it
|
||||
var info = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true);
|
||||
|
||||
Assert.NotNull(info);
|
||||
|
||||
Assert.Equal("Updated Collection Name", info.Name);
|
||||
|
||||
var groups = info.Groups.ToArray();
|
||||
|
||||
Assert.Equal(2, groups.Length);
|
||||
|
||||
var actualGroup2 = Assert.Single(groups.Where(g => g.Id == group2.Id));
|
||||
|
||||
Assert.True(actualGroup2.Manage);
|
||||
Assert.True(actualGroup2.HidePasswords);
|
||||
Assert.False(actualGroup2.ReadOnly);
|
||||
|
||||
var actualGroup3 = Assert.Single(groups.Where(g => g.Id == group3.Id));
|
||||
|
||||
Assert.False(actualGroup3.Manage);
|
||||
Assert.False(actualGroup3.HidePasswords);
|
||||
Assert.True(actualGroup3.ReadOnly);
|
||||
|
||||
var users = info.Users.ToArray();
|
||||
|
||||
Assert.Equal(2, users.Length);
|
||||
|
||||
var actualOrgUser2 = Assert.Single(users.Where(u => u.Id == orgUser2.Id));
|
||||
|
||||
Assert.False(actualOrgUser2.Manage);
|
||||
Assert.False(actualOrgUser2.HidePasswords);
|
||||
Assert.True(actualOrgUser2.ReadOnly);
|
||||
|
||||
var actualOrgUser3 = Assert.Single(users.Where(u => u.Id == orgUser3.Id));
|
||||
|
||||
Assert.True(actualOrgUser3.Manage);
|
||||
Assert.False(actualOrgUser3.HidePasswords);
|
||||
Assert.True(actualOrgUser3.ReadOnly);
|
||||
|
||||
// Clean up data
|
||||
await userRepository.DeleteAsync(user);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await groupRepository.DeleteManyAsync([group1.Id, group2.Id, group3.Id]);
|
||||
await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id, orgUser3.Id]);
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
|
||||
|
||||
public class OrganizationUserReplaceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifically tests OrganizationUsers in the invited state, which is unique because
|
||||
/// they're not linked to a UserId.
|
||||
/// </summary>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsInvited_Success(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var orgUser = await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization);
|
||||
|
||||
// Act: update the user, including collection access so we test this overloaded method
|
||||
orgUser.Type = OrganizationUserType.Admin;
|
||||
orgUser.AccessSecretsManager = true;
|
||||
var collection = await collectionRepository.CreateTestCollectionAsync(organization);
|
||||
|
||||
await organizationUserRepository.ReplaceAsync(orgUser, [
|
||||
new CollectionAccessSelection { Id = collection.Id, Manage = true }
|
||||
]);
|
||||
|
||||
// Assert
|
||||
var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id);
|
||||
Assert.NotNull(actualOrgUser);
|
||||
Assert.Equal(OrganizationUserType.Admin, actualOrgUser.Type);
|
||||
Assert.True(actualOrgUser.AccessSecretsManager);
|
||||
|
||||
var collectionAccess = Assert.Single(actualCollections);
|
||||
Assert.Equal(collection.Id, collectionAccess.Id);
|
||||
Assert.True(collectionAccess.Manage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests OrganizationUsers in the Confirmed status, which is a stand-in for all other
|
||||
/// non-Invited statuses (which are all linked to a UserId).
|
||||
/// </summary>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsConfirmed_Success(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
// OrganizationUser is linked with the User in the Confirmed status
|
||||
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
|
||||
|
||||
// Act: update the user, including collection access so we test this overloaded method
|
||||
orgUser.Type = OrganizationUserType.Admin;
|
||||
orgUser.AccessSecretsManager = true;
|
||||
var collection = await collectionRepository.CreateTestCollectionAsync(organization);
|
||||
|
||||
await organizationUserRepository.ReplaceAsync(orgUser, [
|
||||
new CollectionAccessSelection { Id = collection.Id, Manage = true }
|
||||
]);
|
||||
|
||||
// Assert
|
||||
var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id);
|
||||
Assert.NotNull(actualOrgUser);
|
||||
Assert.Equal(OrganizationUserType.Admin, actualOrgUser.Type);
|
||||
Assert.True(actualOrgUser.AccessSecretsManager);
|
||||
|
||||
var collectionAccess = Assert.Single(actualCollections);
|
||||
Assert.Equal(collection.Id, collectionAccess.Id);
|
||||
Assert.True(collectionAccess.Manage);
|
||||
|
||||
// Account revision date should be updated to a later date
|
||||
var actualUser = await userRepository.GetByIdAsync(user.Id);
|
||||
Assert.NotNull(actualUser);
|
||||
Assert.True(actualUser.AccountRevisionDate.CompareTo(user.AccountRevisionDate) > 0);
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories;
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
|
||||
|
||||
public class OrganizationUserRepositoryTests
|
||||
{
|
@ -5,10 +5,8 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using HandlebarsDotNet;
|
||||
using LinqToDB;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -98,7 +96,7 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
{ "grant_type", "password" },
|
||||
{ "username", username },
|
||||
{ "password", password },
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(username)));
|
||||
}));
|
||||
|
||||
return context;
|
||||
}
|
||||
@ -126,7 +124,7 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
{ "TwoFactorToken", twoFactorToken },
|
||||
{ "TwoFactorProvider", twoFactorProviderType },
|
||||
{ "TwoFactorRemember", "1" },
|
||||
}), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(username)));
|
||||
}));
|
||||
|
||||
return context;
|
||||
}
|
||||
|
@ -4,12 +4,10 @@ using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -37,14 +35,19 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
||||
/// <remarks>
|
||||
/// This will need to be set BEFORE using the <c>Server</c> property
|
||||
/// </remarks>
|
||||
public SqliteConnection? SqliteConnection { get; set; }
|
||||
public ITestDatabase TestDatabase { get; set; } = new SqliteTestDatabase();
|
||||
|
||||
/// <summary>
|
||||
/// If set to <c>true</c> the factory will manage the database lifecycle, including migrations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will need to be set BEFORE using the <c>Server</c> property
|
||||
/// </remarks>
|
||||
public bool ManagesDatabase { get; set; } = true;
|
||||
|
||||
private readonly List<Action<IServiceCollection>> _configureTestServices = new();
|
||||
private readonly List<Action<IConfigurationBuilder>> _configureAppConfiguration = new();
|
||||
|
||||
private bool _handleSqliteDisposal { get; set; }
|
||||
|
||||
|
||||
public void SubstituteService<TService>(Action<TService> mockService)
|
||||
where TService : class
|
||||
{
|
||||
@ -119,12 +122,41 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
||||
/// </summary>
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
if (SqliteConnection == null)
|
||||
var config = new Dictionary<string, string?>
|
||||
{
|
||||
SqliteConnection = new SqliteConnection("DataSource=:memory:");
|
||||
SqliteConnection.Open();
|
||||
_handleSqliteDisposal = true;
|
||||
}
|
||||
// Manually insert a EF provider so that ConfigureServices will add EF repositories but we will override
|
||||
// DbContextOptions to use an in memory database
|
||||
{ "globalSettings:databaseProvider", "postgres" },
|
||||
{ "globalSettings:postgreSql:connectionString", "Host=localhost;Username=test;Password=test;Database=test" },
|
||||
|
||||
// Clear the redis connection string for distributed caching, forcing an in-memory implementation
|
||||
{ "globalSettings:redis:connectionString", "" },
|
||||
|
||||
// Clear Storage
|
||||
{ "globalSettings:attachment:connectionString", null },
|
||||
{ "globalSettings:events:connectionString", null },
|
||||
{ "globalSettings:send:connectionString", null },
|
||||
{ "globalSettings:notifications:connectionString", null },
|
||||
{ "globalSettings:storage:connectionString", null },
|
||||
|
||||
// This will force it to use an ephemeral key for IdentityServer
|
||||
{ "globalSettings:developmentDirectory", null },
|
||||
|
||||
// Email Verification
|
||||
{ "globalSettings:enableEmailVerification", "true" },
|
||||
{ "globalSettings:disableUserRegistration", "false" },
|
||||
{ "globalSettings:launchDarkly:flagValues:email-verification", "true" },
|
||||
|
||||
// New Device Verification
|
||||
{ "globalSettings:disableEmailNewDevice", "false" },
|
||||
|
||||
// Web push notifications
|
||||
{ "globalSettings:webPush:vapidPublicKey", "BGBtAM0bU3b5jsB14IjBYarvJZ6rWHilASLudTTYDDBi7a-3kebo24Yus_xYeOMZ863flAXhFAbkL6GVSrxgErg" },
|
||||
{ "globalSettings:launchDarkly:flagValues:web-push", "true" },
|
||||
};
|
||||
|
||||
// Some database drivers modify the connection string
|
||||
TestDatabase.ModifyGlobalSettings(config);
|
||||
|
||||
builder.ConfigureAppConfiguration(c =>
|
||||
{
|
||||
@ -134,39 +166,7 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
||||
|
||||
c.AddUserSecrets(typeof(Identity.Startup).Assembly, optional: true);
|
||||
|
||||
c.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
// Manually insert a EF provider so that ConfigureServices will add EF repositories but we will override
|
||||
// DbContextOptions to use an in memory database
|
||||
{ "globalSettings:databaseProvider", "postgres" },
|
||||
{ "globalSettings:postgreSql:connectionString", "Host=localhost;Username=test;Password=test;Database=test" },
|
||||
|
||||
// Clear the redis connection string for distributed caching, forcing an in-memory implementation
|
||||
{ "globalSettings:redis:connectionString", ""},
|
||||
|
||||
// Clear Storage
|
||||
{ "globalSettings:attachment:connectionString", null},
|
||||
{ "globalSettings:events:connectionString", null},
|
||||
{ "globalSettings:send:connectionString", null},
|
||||
{ "globalSettings:notifications:connectionString", null},
|
||||
{ "globalSettings:storage:connectionString", null},
|
||||
|
||||
// This will force it to use an ephemeral key for IdentityServer
|
||||
{ "globalSettings:developmentDirectory", null },
|
||||
|
||||
|
||||
// Email Verification
|
||||
{ "globalSettings:enableEmailVerification", "true" },
|
||||
{ "globalSettings:disableUserRegistration", "false" },
|
||||
{ "globalSettings:launchDarkly:flagValues:email-verification", "true" },
|
||||
|
||||
// New Device Verification
|
||||
{ "globalSettings:disableEmailNewDevice", "false" },
|
||||
|
||||
// Web push notifications
|
||||
{ "globalSettings:webPush:vapidPublicKey", "BGBtAM0bU3b5jsB14IjBYarvJZ6rWHilASLudTTYDDBi7a-3kebo24Yus_xYeOMZ863flAXhFAbkL6GVSrxgErg" },
|
||||
{ "globalSettings:launchDarkly:flagValues:web-push", "true" },
|
||||
});
|
||||
c.AddInMemoryCollection(config);
|
||||
});
|
||||
|
||||
// Run configured actions after defaults to allow them to take precedence
|
||||
@ -177,17 +177,16 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
||||
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
var dbContextOptions = services.First(sd => sd.ServiceType == typeof(DbContextOptions<DatabaseContext>));
|
||||
var dbContextOptions =
|
||||
services.First(sd => sd.ServiceType == typeof(DbContextOptions<DatabaseContext>));
|
||||
services.Remove(dbContextOptions);
|
||||
services.AddScoped(services =>
|
||||
{
|
||||
return new DbContextOptionsBuilder<DatabaseContext>()
|
||||
.UseSqlite(SqliteConnection)
|
||||
.UseApplicationServiceProvider(services)
|
||||
.Options;
|
||||
});
|
||||
|
||||
MigrateDbContext<DatabaseContext>(services);
|
||||
// Add database to the service collection
|
||||
TestDatabase.AddDatabase(services);
|
||||
if (ManagesDatabase)
|
||||
{
|
||||
TestDatabase.Migrate(services);
|
||||
}
|
||||
|
||||
// QUESTION: The normal licensing service should run fine on developer machines but not in CI
|
||||
// should we have a fork here to leave the normal service for developers?
|
||||
@ -209,9 +208,6 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
||||
// TODO: Install and use azurite in CI pipeline
|
||||
Replace<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>(services);
|
||||
|
||||
// TODO: Install and use azurite in CI pipeline
|
||||
Replace<IReferenceEventService, NoopReferenceEventService>(services);
|
||||
|
||||
// Our Rate limiter works so well that it begins to fail tests unless we carve out
|
||||
// one whitelisted ip. We should still test the rate limiter though and they should change the Ip
|
||||
// to something that is NOT whitelisted
|
||||
@ -286,22 +282,11 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (_handleSqliteDisposal)
|
||||
if (ManagesDatabase)
|
||||
{
|
||||
SqliteConnection!.Dispose();
|
||||
// Avoid calling Dispose twice
|
||||
ManagesDatabase = false;
|
||||
TestDatabase.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void MigrateDbContext<TContext>(IServiceCollection serviceCollection) where TContext : DbContext
|
||||
{
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var services = scope.ServiceProvider;
|
||||
var context = services.GetRequiredService<TContext>();
|
||||
if (_handleSqliteDisposal)
|
||||
{
|
||||
context.Database.EnsureDeleted();
|
||||
}
|
||||
context.Database.EnsureCreated();
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Net;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
@ -62,12 +61,6 @@ public static class WebApplicationFactoryExtensions
|
||||
Action<HttpContext> extraConfiguration = null)
|
||||
=> SendAsync(server, HttpMethod.Delete, requestUri, content: content, extraConfiguration);
|
||||
|
||||
public static HttpContext SetAuthEmail(this HttpContext context, string username)
|
||||
{
|
||||
context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(username));
|
||||
return context;
|
||||
}
|
||||
|
||||
public static HttpContext SetIp(this HttpContext context, string ip)
|
||||
{
|
||||
context.Connection.RemoteIpAddress = IPAddress.Parse(ip);
|
||||
|
19
test/IntegrationTestCommon/ITestDatabase.cs
Normal file
19
test/IntegrationTestCommon/ITestDatabase.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.IntegrationTestCommon;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public interface ITestDatabase
|
||||
{
|
||||
public void AddDatabase(IServiceCollection serviceCollection);
|
||||
|
||||
public void Migrate(IServiceCollection serviceCollection);
|
||||
|
||||
public void Dispose();
|
||||
|
||||
public void ModifyGlobalSettings(Dictionary<string, string?> config)
|
||||
{
|
||||
// Default implementation does nothing
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Identity\Identity.csproj" />
|
||||
<ProjectReference Include="..\..\util\Migrator\Migrator.csproj" />
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
83
test/IntegrationTestCommon/SqlServerTestDatabase.cs
Normal file
83
test/IntegrationTestCommon/SqlServerTestDatabase.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Migrator;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.IntegrationTestCommon;
|
||||
|
||||
public class SqlServerTestDatabase : ITestDatabase
|
||||
{
|
||||
private string _sqlServerConnection { get; set; }
|
||||
|
||||
public SqlServerTestDatabase()
|
||||
{
|
||||
// Grab the connection string from the Identity project user secrets
|
||||
var identityBuilder = new ConfigurationBuilder();
|
||||
identityBuilder.AddUserSecrets(typeof(Identity.Startup).Assembly, optional: true);
|
||||
var identityConfig = identityBuilder.Build();
|
||||
var identityConnectionString = identityConfig.GetSection("globalSettings:sqlServer:connectionString").Value;
|
||||
|
||||
// Replace the database name in the connection string to use a test database
|
||||
var testConnectionString = new SqlConnectionStringBuilder(identityConnectionString)
|
||||
{
|
||||
InitialCatalog = "vault_test"
|
||||
}.ConnectionString;
|
||||
|
||||
_sqlServerConnection = testConnectionString;
|
||||
}
|
||||
|
||||
public void ModifyGlobalSettings(Dictionary<string, string> config)
|
||||
{
|
||||
config["globalSettings:databaseProvider"] = "sqlserver";
|
||||
config["globalSettings:sqlServer:connectionString"] = _sqlServerConnection;
|
||||
}
|
||||
|
||||
public void AddDatabase(IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddScoped(s => new DbContextOptionsBuilder<DatabaseContext>()
|
||||
.UseSqlServer(_sqlServerConnection)
|
||||
.UseApplicationServiceProvider(s)
|
||||
.Options);
|
||||
}
|
||||
|
||||
public void Migrate(IServiceCollection serviceCollection)
|
||||
{
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var services = scope.ServiceProvider;
|
||||
var globalSettings = services.GetRequiredService<GlobalSettings>();
|
||||
var logger = services.GetRequiredService<ILogger<DbMigrator>>();
|
||||
|
||||
var migrator = new SqlServerDbMigrator(globalSettings, logger);
|
||||
migrator.MigrateDatabase();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
var masterConnectionString = new SqlConnectionStringBuilder(_sqlServerConnection)
|
||||
{
|
||||
InitialCatalog = "master"
|
||||
}.ConnectionString;
|
||||
|
||||
using var connection = new SqlConnection(masterConnectionString);
|
||||
var databaseName = new SqlConnectionStringBuilder(_sqlServerConnection).InitialCatalog;
|
||||
|
||||
connection.Open();
|
||||
|
||||
var databaseNameQuoted = new SqlCommandBuilder().QuoteIdentifier(databaseName);
|
||||
|
||||
using (var cmd = new SqlCommand($"ALTER DATABASE {databaseNameQuoted} SET single_user WITH rollback IMMEDIATE", connection))
|
||||
{
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var cmd = new SqlCommand($"DROP DATABASE {databaseNameQuoted}", connection))
|
||||
{
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
41
test/IntegrationTestCommon/SqliteTestDatabase.cs
Normal file
41
test/IntegrationTestCommon/SqliteTestDatabase.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.IntegrationTestCommon;
|
||||
|
||||
public class SqliteTestDatabase : ITestDatabase
|
||||
{
|
||||
private SqliteConnection SqliteConnection { get; set; }
|
||||
|
||||
public SqliteTestDatabase()
|
||||
{
|
||||
SqliteConnection = new SqliteConnection("DataSource=:memory:");
|
||||
SqliteConnection.Open();
|
||||
}
|
||||
|
||||
public void AddDatabase(IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddScoped(s => new DbContextOptionsBuilder<DatabaseContext>()
|
||||
.UseSqlite(SqliteConnection)
|
||||
.UseApplicationServiceProvider(s)
|
||||
.Options);
|
||||
}
|
||||
|
||||
public void Migrate(IServiceCollection serviceCollection)
|
||||
{
|
||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var services = scope.ServiceProvider;
|
||||
var context = services.GetRequiredService<DatabaseContext>();
|
||||
|
||||
context.Database.EnsureDeleted();
|
||||
context.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SqliteConnection.Dispose();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user