mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
Merge branch 'main' into auth/pm-20348/extension-auth-approvals-add-auth-request-endpoint
This commit is contained in:
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,15 +1,16 @@
|
||||
using AutoFixture;
|
||||
using Bit.Api.Tools.Controllers;
|
||||
using Bit.Api.Dirt.Controllers;
|
||||
using Bit.Api.Dirt.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Tools.Controllers;
|
||||
namespace Bit.Api.Test.Dirt;
|
||||
|
||||
|
||||
[ControllerCustomize(typeof(ReportsController))]
|
||||
@ -54,7 +55,7 @@ public class ReportsControllerTests
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
|
||||
|
||||
// Act
|
||||
var request = new Api.Tools.Models.PasswordHealthReportApplicationModel
|
||||
var request = new PasswordHealthReportApplicationModel
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
Url = "https://example.com",
|
||||
@ -77,7 +78,7 @@ public class ReportsControllerTests
|
||||
|
||||
// Act
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.CreateMany<Api.Tools.Models.PasswordHealthReportApplicationModel>(2);
|
||||
var request = fixture.CreateMany<PasswordHealthReportApplicationModel>(2);
|
||||
await sutProvider.Sut.AddPasswordHealthReportApplications(request);
|
||||
|
||||
// Assert
|
||||
@ -93,7 +94,7 @@ public class ReportsControllerTests
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
|
||||
|
||||
// Act
|
||||
var request = new Api.Tools.Models.PasswordHealthReportApplicationModel
|
||||
var request = new PasswordHealthReportApplicationModel
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
Url = "https://example.com",
|
||||
@ -114,7 +115,7 @@ public class ReportsControllerTests
|
||||
|
||||
// Act
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<Api.Tools.Models.PasswordHealthReportApplicationModel>();
|
||||
var request = fixture.Create<PasswordHealthReportApplicationModel>();
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||
await sutProvider.Sut.AddPasswordHealthReportApplication(request));
|
||||
|
||||
|
@ -175,7 +175,7 @@ public class AccountsKeyManagementControllerTests
|
||||
}
|
||||
catch (BadRequestException ex)
|
||||
{
|
||||
Assert.NotEmpty(ex.ModelState.Values);
|
||||
Assert.NotEmpty(ex.ModelState!.Values);
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,7 +210,7 @@ public class AccountsKeyManagementControllerTests
|
||||
var badRequestException =
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
|
||||
|
||||
Assert.Equal(1, badRequestException.ModelState.ErrorCount);
|
||||
Assert.Equal(1, badRequestException.ModelState!.ErrorCount);
|
||||
Assert.Equal("set key connector key error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
|
||||
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||
.SetKeyConnectorKeyAsync(Arg.Do<User>(user =>
|
||||
@ -284,7 +284,7 @@ public class AccountsKeyManagementControllerTests
|
||||
var badRequestException =
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync());
|
||||
|
||||
Assert.Equal(1, badRequestException.ModelState.ErrorCount);
|
||||
Assert.Equal(1, badRequestException.ModelState!.ErrorCount);
|
||||
Assert.Equal("convert to key connector error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
|
||||
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser));
|
||||
|
@ -3,7 +3,6 @@ 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;
|
||||
@ -33,7 +32,6 @@ public class SendsControllerTests : IDisposable
|
||||
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||
private readonly ISendFileStorageService _sendFileStorageService;
|
||||
private readonly ILogger<SendsController> _logger;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public SendsControllerTests()
|
||||
{
|
||||
@ -45,7 +43,6 @@ public class SendsControllerTests : IDisposable
|
||||
_sendFileStorageService = Substitute.For<ISendFileStorageService>();
|
||||
_globalSettings = new GlobalSettings();
|
||||
_logger = Substitute.For<ILogger<SendsController>>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
|
||||
_sut = new SendsController(
|
||||
_sendRepository,
|
||||
@ -55,8 +52,7 @@ public class SendsControllerTests : IDisposable
|
||||
_nonAnonymousSendCommand,
|
||||
_sendFileStorageService,
|
||||
_logger,
|
||||
_globalSettings,
|
||||
_currentContext
|
||||
_globalSettings
|
||||
);
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,8 +4,19 @@ using AutoFixture.Kernel;
|
||||
|
||||
namespace Bit.Test.Common.AutoFixture;
|
||||
|
||||
/// <summary>
|
||||
/// A utility class that encapsulates a system under test (sut) and its dependencies.
|
||||
/// By default, all dependencies are initialized as mocks using the NSubstitute library.
|
||||
/// SutProvider provides an interface for accessing these dependencies in the arrange and assert stages of your tests.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSut">The concrete implementation of the class being tested.</typeparam>
|
||||
public class SutProvider<TSut> : ISutProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// A record of the configured dependencies (constructor parameters). The outer Dictionary is keyed by the dependency's
|
||||
/// type, and the inner dictionary is keyed by the parameter name (optionally used to disambiguate parameters with the same type).
|
||||
/// The inner dictionary value is the dependency.
|
||||
/// </summary>
|
||||
private Dictionary<Type, Dictionary<string, object>> _dependencies;
|
||||
private readonly IFixture _fixture;
|
||||
private readonly ConstructorParameterRelay<TSut> _constructorParameterRelay;
|
||||
@ -23,13 +34,25 @@ public class SutProvider<TSut> : ISutProvider
|
||||
_fixture.Customizations.Add(_constructorParameterRelay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a dependency to be injected when the sut is created. You must call <see cref="Create"/> after
|
||||
/// this method to (re)create the sut with the dependency.
|
||||
/// </summary>
|
||||
/// <param name="dependency">The dependency to register.</param>
|
||||
/// <param name="parameterName">An optional parameter name to disambiguate the dependency if there are multiple of the same type. You generally don't need this.</param>
|
||||
/// <typeparam name="T">The type to register the dependency under - usually an interface. This should match the type expected by the sut's constructor.</typeparam>
|
||||
/// <returns></returns>
|
||||
public SutProvider<TSut> SetDependency<T>(T dependency, string parameterName = "")
|
||||
=> SetDependency(typeof(T), dependency, parameterName);
|
||||
public SutProvider<TSut> SetDependency(Type dependencyType, object dependency, string parameterName = "")
|
||||
|
||||
/// <summary>
|
||||
/// An overload for <see cref="SetDependency{T}"/> which takes a runtime <see cref="Type"/> object rather than a compile-time type.
|
||||
/// </summary>
|
||||
private 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
|
||||
{
|
||||
@ -39,46 +62,69 @@ public class SutProvider<TSut> : ISutProvider
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a dependency of the sut. Can only be called after the dependency has been set, either explicitly with
|
||||
/// <see cref="SetDependency{T}"/> or automatically with <see cref="Create"/>.
|
||||
/// As dependencies are initialized with NSubstitute mocks by default, this is often used to retrieve those mocks in order to
|
||||
/// configure them during the arrange stage, or check received calls in the assert stage.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">An optional parameter name to disambiguate the dependency if there are multiple of the same type. You generally don't need this.</param>
|
||||
/// <typeparam name="T">The type of the dependency you want to get - usually an interface.</typeparam>
|
||||
/// <returns>The dependency.</returns>
|
||||
public T GetDependency<T>(string parameterName = "") => (T)GetDependency(typeof(T), parameterName);
|
||||
public object GetDependency(Type dependencyType, string parameterName = "")
|
||||
|
||||
/// <summary>
|
||||
/// An overload for <see cref="GetDependency{T}"/> which takes a runtime <see cref="Type"/> object rather than a compile-time type.
|
||||
/// </summary>
|
||||
private object GetDependency(Type dependencyType, string parameterName = "")
|
||||
{
|
||||
if (DependencyIsSet(dependencyType, parameterName))
|
||||
{
|
||||
return _dependencies[dependencyType][parameterName];
|
||||
}
|
||||
else if (_dependencies.ContainsKey(dependencyType))
|
||||
|
||||
if (_dependencies.TryGetValue(dependencyType, out var knownDependencies))
|
||||
{
|
||||
var knownDependencies = _dependencies[dependencyType];
|
||||
if (knownDependencies.Values.Count == 1)
|
||||
{
|
||||
return _dependencies[dependencyType].Values.Single();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(string.Concat($"Dependency of type {dependencyType.Name} and name ",
|
||||
$"{parameterName} does not exist. Available dependency names are: ",
|
||||
string.Join(", ", knownDependencies.Keys)));
|
||||
return knownDependencies.Values.Single();
|
||||
}
|
||||
|
||||
throw new ArgumentException(string.Concat($"Dependency of type {dependencyType.Name} and name ",
|
||||
$"{parameterName} does not exist. Available dependency names are: ",
|
||||
string.Join(", ", knownDependencies.Keys)));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Dependency of type {dependencyType.Name} and name {parameterName} has not been set.");
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Dependency of type {dependencyType.Name} and name {parameterName} has not been set.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all the dependencies and the sut. This reverts the SutProvider back to a fully uninitialized state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_dependencies = new Dictionary<Type, Dictionary<string, object>>();
|
||||
Sut = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreate a new sut with all new dependencies. This will reset all dependencies, including mocked return values
|
||||
/// and any dependencies set with <see cref="SetDependency{T}"/>.
|
||||
/// </summary>
|
||||
public void Recreate()
|
||||
{
|
||||
_dependencies = new Dictionary<Type, Dictionary<string, object>>();
|
||||
Sut = _fixture.Create<TSut>();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Create()"/>>
|
||||
ISutProvider ISutProvider.Create() => Create();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the sut, injecting any dependencies configured via <see cref="SetDependency{T}"/> and falling back to
|
||||
/// NSubstitute mocks for any dependencies that have not been explicitly configured.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public SutProvider<TSut> Create()
|
||||
{
|
||||
Sut = _fixture.Create<TSut>();
|
||||
@ -90,6 +136,19 @@ public class SutProvider<TSut> : ISutProvider
|
||||
|
||||
private object GetDefault(Type type) => type.IsValueType ? Activator.CreateInstance(type) : null;
|
||||
|
||||
/// <summary>
|
||||
/// A specimen builder which tells Autofixture to use the dependency registered in <see cref="SutProvider{T}"/>
|
||||
/// when creating test data. If no matching dependency exists in <see cref="SutProvider{TSut}"/>, it creates
|
||||
/// an NSubstitute mock and registers it using <see cref="SutProvider{TSut}.SetDependency{T}"/>
|
||||
/// so it can be retrieved later.
|
||||
/// This is the link between <see cref="SutProvider{T}"/> and Autofixture.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Autofixture knows how to create sample data of simple types (such as an int or string) but not more complex classes.
|
||||
/// We create our own <see cref="ISpecimenBuilder"/> and register it with the <see cref="Fixture"/> in
|
||||
/// <see cref="SutProvider{TSut}"/> to provide that instruction.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of the sut.</typeparam>
|
||||
private class ConstructorParameterRelay<T> : ISpecimenBuilder
|
||||
{
|
||||
private readonly SutProvider<T> _sutProvider;
|
||||
@ -103,6 +162,7 @@ public class SutProvider<TSut> : ISutProvider
|
||||
|
||||
public object Create(object request, ISpecimenContext context)
|
||||
{
|
||||
// Basic checks to filter out irrelevant requests from Autofixture
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
@ -117,16 +177,22 @@ public class SutProvider<TSut> : ISutProvider
|
||||
return new NoSpecimen();
|
||||
}
|
||||
|
||||
// Use the dependency set under this parameter name, if any
|
||||
if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, parameterInfo.Name))
|
||||
{
|
||||
return _sutProvider.GetDependency(parameterInfo.ParameterType, parameterInfo.Name);
|
||||
}
|
||||
// Return default type if set
|
||||
else if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, ""))
|
||||
|
||||
// Use the default dependency set for this type, if any (i.e. no parameter name has been specified)
|
||||
if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, ""))
|
||||
{
|
||||
return _sutProvider.GetDependency(parameterInfo.ParameterType, "");
|
||||
}
|
||||
|
||||
// Fallback: pass the request down the chain. This lets another fixture customization populate the value.
|
||||
// If you haven't added any customizations, this should be an NSubstitute mock.
|
||||
// It is registered with SetDependency so you can retrieve it later.
|
||||
|
||||
// This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for
|
||||
// Create(Type type) exists.
|
||||
var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType,
|
||||
|
@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.5.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0868" />
|
||||
|
@ -0,0 +1,84 @@
|
||||
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
|
||||
{
|
||||
private const string _messageId = "TestMessageId";
|
||||
|
||||
[Fact]
|
||||
public void ApplyRetry_IncrementsRetryCountAndSetsDelayUntilDate()
|
||||
{
|
||||
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
|
||||
MessageId = _messageId,
|
||||
RetryCount = 2,
|
||||
RenderedTemplate = string.Empty,
|
||||
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"),
|
||||
MessageId = _messageId,
|
||||
RenderedTemplate = "This is the message",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RetryCount = 2,
|
||||
DelayUntilDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var json = message.ToJson();
|
||||
var result = IntegrationMessage<WebhookIntegrationConfigurationDetails>.FromJson(json);
|
||||
|
||||
Assert.Equal(message.Configuration, result.Configuration);
|
||||
Assert.Equal(message.MessageId, result.MessageId);
|
||||
Assert.Equal(message.RenderedTemplate, result.RenderedTemplate);
|
||||
Assert.Equal(message.IntegrationType, result.IntegrationType);
|
||||
Assert.Equal(message.RetryCount, result.RetryCount);
|
||||
Assert.Equal(message.DelayUntilDate, result.DelayUntilDate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_InvalidJson_ThrowsJsonException()
|
||||
{
|
||||
var json = "{ Invalid JSON";
|
||||
Assert.Throws<JsonException>(() => IntegrationMessage<WebhookIntegrationConfigurationDetails>.FromJson(json));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToJson_BaseIntegrationMessage_DeserializesCorrectly()
|
||||
{
|
||||
var message = new IntegrationMessage
|
||||
{
|
||||
MessageId = _messageId,
|
||||
RenderedTemplate = "This is the message",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RetryCount = 2,
|
||||
DelayUntilDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var json = message.ToJson();
|
||||
var result = JsonSerializer.Deserialize<IntegrationMessage>(json);
|
||||
|
||||
Assert.Equal(message.MessageId, result.MessageId);
|
||||
Assert.Equal(message.RenderedTemplate, result.RenderedTemplate);
|
||||
Assert.Equal(message.IntegrationType, result.IntegrationType);
|
||||
Assert.Equal(message.RetryCount, result.RetryCount);
|
||||
Assert.Equal(message.DelayUntilDate, result.DelayUntilDate);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -137,6 +137,14 @@ public class InviteOrganizationUserCommandTests
|
||||
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
@ -202,6 +210,14 @@ public class InviteOrganizationUserCommandTests
|
||||
.Returns(new Invalid<InviteOrganizationUsersValidationRequest>(
|
||||
new Error<InviteOrganizationUsersValidationRequest>(errorMessage, validationRequest)));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
@ -272,6 +288,14 @@ public class InviteOrganizationUserCommandTests
|
||||
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||
.WithPasswordManagerUpdate(new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
@ -343,6 +367,14 @@ public class InviteOrganizationUserCommandTests
|
||||
.WithPasswordManagerUpdate(
|
||||
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
@ -413,6 +445,14 @@ public class InviteOrganizationUserCommandTests
|
||||
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||
.WithPasswordManagerUpdate(passwordManagerUpdate)));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
@ -469,6 +509,7 @@ public class InviteOrganizationUserCommandTests
|
||||
.AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager));
|
||||
|
||||
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
|
||||
orgUserRepository
|
||||
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||
@ -476,11 +517,13 @@ public class InviteOrganizationUserCommandTests
|
||||
orgUserRepository
|
||||
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||
.Returns([ownerDetails]);
|
||||
orgUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(1);
|
||||
orgRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
orgUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(1);
|
||||
|
||||
var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
|
||||
orgRepository.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
@ -566,6 +609,14 @@ public class InviteOrganizationUserCommandTests
|
||||
.SendInvitesAsync(Arg.Any<SendInvitesRequest>())
|
||||
.Throws(new Exception("Something went wrong"));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
@ -671,6 +722,14 @@ public class InviteOrganizationUserCommandTests
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
@ -762,6 +821,14 @@ public class InviteOrganizationUserCommandTests
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
@ -829,6 +896,14 @@ public class InviteOrganizationUserCommandTests
|
||||
.WithPasswordManagerUpdate(
|
||||
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
@ -900,6 +975,14 @@ public class InviteOrganizationUserCommandTests
|
||||
.WithPasswordManagerUpdate(
|
||||
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||
|
@ -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;
|
||||
@ -28,7 +31,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
@ -46,7 +54,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
RestoreUser_Setup(organization, null, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
@ -148,7 +161,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
var user = new User();
|
||||
user.Email = "test@bitwarden.com";
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||
@ -181,7 +199,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
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) });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
@ -208,6 +231,62 @@ 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<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
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,
|
||||
@ -224,6 +303,51 @@ public class RestoreOrganizationUserCommandTests
|
||||
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<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
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_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);
|
||||
|
||||
@ -250,6 +374,15 @@ public class RestoreOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(organizationUser.UserId.Value)
|
||||
.Returns(new[] { organizationUser, secondOrganizationUser });
|
||||
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<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns(new[]
|
||||
@ -277,45 +410,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,
|
||||
@ -337,7 +431,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
{
|
||||
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns([
|
||||
@ -364,28 +463,55 @@ 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<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
|
||||
]);
|
||||
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.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
|
||||
}
|
||||
]));
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
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 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()
|
||||
@ -407,7 +533,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
{
|
||||
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<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
|
||||
@ -444,7 +575,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
otherOrganization.PlanType = PlanType.Free;
|
||||
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(organizationUser.UserId.Value)
|
||||
.Returns([orgUserOwnerFromDifferentOrg]);
|
||||
@ -485,7 +621,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
otherOrganization.PlanType = PlanType.Free;
|
||||
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
organizationUserRepository
|
||||
.GetManyByUserAsync(organizationUser.UserId.Value)
|
||||
@ -536,7 +677,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
otherOrganization.PlanType = PlanType.Free;
|
||||
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
organizationUserRepository
|
||||
.GetManyByUserAsync(organizationUser.UserId.Value)
|
||||
@ -588,7 +734,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
organizationUserRepository
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))
|
||||
.Returns([orgUser1, orgUser2]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
twoFactorIsEnabledQuery
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
|
||||
@ -637,7 +788,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
organizationUserRepository
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))
|
||||
.Returns(new[] { orgUser1, orgUser2, orgUser3 });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" });
|
||||
|
||||
// Setup 2FA policy
|
||||
@ -672,6 +828,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,
|
||||
@ -701,7 +928,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
organizationUserRepository
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))
|
||||
.Returns([orgUser1, orgUser2, orgUser3]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" });
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
@ -763,7 +995,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
organizationUserRepository
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id)))
|
||||
.Returns([orgUser1]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
organizationUserRepository
|
||||
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([orgUserFromOtherOrg]);
|
||||
@ -823,7 +1060,12 @@ public class RestoreOrganizationUserCommandTests
|
||||
organizationUserRepository
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id)))
|
||||
.Returns([orgUser1]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
organizationUserRepository
|
||||
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([orgUserFromOtherOrg]);
|
||||
@ -853,7 +1095,14 @@ public class RestoreOrganizationUserCommandTests
|
||||
}
|
||||
targetOrganizationUser.OrganizationId = organization.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner);
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(requestingOrganizationUser != null && (requestingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin));
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -10,9 +10,6 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
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;
|
||||
@ -65,17 +62,6 @@ public class ProviderClientOrganizationSignUpCommandTests
|
||||
)
|
||||
);
|
||||
|
||||
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 &&
|
||||
referenceEvent.SignupInitiationPath == signup.InitiationPath
|
||||
));
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -60,16 +60,19 @@ public class TwoFactorAuthenticationPolicyValidatorTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OnSaveSideEffectsAsync_RevokesNonCompliantUsers(
|
||||
public async Task OnSaveSideEffectsAsync_RevokesOnlyNonCompliantUsers(
|
||||
Organization organization,
|
||||
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
|
||||
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
|
||||
{
|
||||
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
|
||||
// Arrange
|
||||
policy.OrganizationId = policyUpdate.OrganizationId;
|
||||
organization.Id = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails
|
||||
var nonCompliantUser = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
@ -80,30 +83,57 @@ public class TwoFactorAuthenticationPolicyValidatorTests
|
||||
HasMasterPassword = true
|
||||
};
|
||||
|
||||
var compliantUser = new OrganizationUserUserDetails
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
Email = "user4@test.com",
|
||||
Name = "TEST",
|
||||
UserId = Guid.NewGuid(),
|
||||
HasMasterPassword = true
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
|
||||
.Returns([orgUserDetailUserWithout2Fa]);
|
||||
.Returns([nonCompliantUser, compliantUser]);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
|
||||
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
|
||||
{
|
||||
(orgUserDetailUserWithout2Fa, false)
|
||||
(nonCompliantUser, false),
|
||||
(compliantUser, true)
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
|
||||
.Returns(new CommandResult());
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.Received(1)
|
||||
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());
|
||||
|
||||
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||
.Received(1)
|
||||
.RevokeNonCompliantOrganizationUsersAsync(Arg.Is<RevokeOrganizationUsersRequest>(req =>
|
||||
req.OrganizationId == policyUpdate.OrganizationId &&
|
||||
req.OrganizationUsers.SequenceEqual(new[] { nonCompliantUser })
|
||||
));
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),
|
||||
"user3@test.com");
|
||||
nonCompliantUser.Email);
|
||||
|
||||
// Did not send out an email for compliantUser
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(0)
|
||||
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),
|
||||
compliantUser.Email);
|
||||
}
|
||||
}
|
||||
|
@ -288,7 +288,7 @@ public class SavePolicyCommandTests
|
||||
{
|
||||
return new SutProvider<SavePolicyCommand>()
|
||||
.WithFakeTimeProvider()
|
||||
.SetDependency(typeof(IEnumerable<IPolicyValidator>), policyValidators ?? [])
|
||||
.SetDependency(policyValidators ?? [])
|
||||
.Create();
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,133 @@
|
||||
using System.Text.Json;
|
||||
using Azure.Messaging.ServiceBus;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AzureServiceBusEventListenerServiceTests
|
||||
{
|
||||
private readonly IEventMessageHandler _handler = Substitute.For<IEventMessageHandler>();
|
||||
private readonly ILogger<AzureServiceBusEventListenerService> _logger =
|
||||
Substitute.For<ILogger<AzureServiceBusEventListenerService>>();
|
||||
private const string _messageId = "messageId";
|
||||
|
||||
private SutProvider<AzureServiceBusEventListenerService> GetSutProvider()
|
||||
{
|
||||
return new SutProvider<AzureServiceBusEventListenerService>()
|
||||
.SetDependency(_handler)
|
||||
.SetDependency(_logger)
|
||||
.SetDependency("test-subscription", "subscriptionName")
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
|
||||
await sutProvider.Sut.ProcessErrorAsync(args);
|
||||
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessReceivedMessageAsync_EmptyJson_LogsError()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(string.Empty, _messageId);
|
||||
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<JsonException>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync("{ Inavlid JSON }", _messageId);
|
||||
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString().Contains("Invalid JSON")),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessReceivedMessageAsync_InvalidJsonArray_LogsError()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(
|
||||
"{ \"not a valid\", \"list of event messages\" }",
|
||||
_messageId
|
||||
);
|
||||
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<JsonException>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessReceivedMessageAsync_InvalidJsonObject_LogsError()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(
|
||||
JsonSerializer.Serialize(DateTime.UtcNow), // wrong object - not EventMessage
|
||||
_messageId
|
||||
);
|
||||
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<JsonException>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessReceivedMessageAsync_SingleEvent_DelegatesToHandler(EventMessage message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(
|
||||
JsonSerializer.Serialize(message),
|
||||
_messageId
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleEventAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message, new[] { "IdempotencyId" })));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessReceivedMessageAsync_ManyEvents_DelegatesToHandler(IEnumerable<EventMessage> messages)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(
|
||||
JsonSerializer.Serialize(messages),
|
||||
_messageId
|
||||
);
|
||||
|
||||
await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleManyEventsAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(messages, new[] { "IdempotencyId" })));
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
#nullable enable
|
||||
|
||||
using Azure.Messaging.ServiceBus;
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AzureServiceBusIntegrationListenerServiceTests
|
||||
{
|
||||
private const int _maxRetries = 3;
|
||||
private const string _topicName = "test_topic";
|
||||
private const string _subscriptionName = "test_subscription";
|
||||
private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>();
|
||||
private readonly IAzureServiceBusService _serviceBusService = Substitute.For<IAzureServiceBusService>();
|
||||
private readonly ILogger<AzureServiceBusIntegrationListenerService> _logger =
|
||||
Substitute.For<ILogger<AzureServiceBusIntegrationListenerService>>();
|
||||
|
||||
private SutProvider<AzureServiceBusIntegrationListenerService> GetSutProvider()
|
||||
{
|
||||
return new SutProvider<AzureServiceBusIntegrationListenerService>()
|
||||
.SetDependency(_handler)
|
||||
.SetDependency(_serviceBusService)
|
||||
.SetDependency(_topicName, "topicName")
|
||||
.SetDependency(_subscriptionName, "subscriptionName")
|
||||
.SetDependency(_maxRetries, "maxRetries")
|
||||
.SetDependency(_logger)
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
await sutProvider.Sut.ProcessErrorAsync(args);
|
||||
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.RetryCount = 0;
|
||||
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = false;
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = (IntegrationMessage<WebhookIntegrationConfiguration>)IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson())!;
|
||||
|
||||
Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));
|
||||
|
||||
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
|
||||
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.RetryCount = _maxRetries;
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = (IntegrationMessage<WebhookIntegrationConfiguration>)IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson())!;
|
||||
|
||||
Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));
|
||||
|
||||
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
|
||||
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.RetryCount = 0;
|
||||
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1);
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = (IntegrationMessage<WebhookIntegrationConfiguration>)IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson())!;
|
||||
|
||||
Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));
|
||||
|
||||
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
|
||||
await _serviceBusService.Received(1).PublishToRetryAsync(message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
var result = new IntegrationHandlerResult(true, message);
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = (IntegrationMessage<WebhookIntegrationConfiguration>)IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson())!;
|
||||
|
||||
Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));
|
||||
|
||||
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
|
||||
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class EventIntegrationEventWriteServiceTests
|
||||
{
|
||||
private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();
|
||||
private readonly EventIntegrationEventWriteService Subject;
|
||||
|
||||
public EventIntegrationEventWriteServiceTests()
|
||||
{
|
||||
Subject = new EventIntegrationEventWriteService(_eventIntegrationPublisher);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_EventPublishedToEventQueue(EventMessage eventMessage)
|
||||
{
|
||||
var expected = JsonSerializer.Serialize(eventMessage);
|
||||
await Subject.CreateAsync(eventMessage);
|
||||
await _eventIntegrationPublisher.Received(1).PublishEventAsync(
|
||||
Arg.Is<string>(body => AssertJsonStringsMatch(eventMessage, body)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateManyAsync_EventsPublishedToEventQueue(IEnumerable<EventMessage> eventMessages)
|
||||
{
|
||||
await Subject.CreateManyAsync(eventMessages);
|
||||
await _eventIntegrationPublisher.Received(1).PublishEventAsync(
|
||||
Arg.Is<string>(body => AssertJsonStringsMatch(eventMessages, body)));
|
||||
}
|
||||
|
||||
private static bool AssertJsonStringsMatch(EventMessage expected, string body)
|
||||
{
|
||||
var actual = JsonSerializer.Deserialize<EventMessage>(body);
|
||||
AssertHelper.AssertPropertyEqual(expected, actual, new[] { "IdempotencyId" });
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool AssertJsonStringsMatch(IEnumerable<EventMessage> expected, string body)
|
||||
{
|
||||
using var actual = JsonSerializer.Deserialize<IEnumerable<EventMessage>>(body).GetEnumerator();
|
||||
|
||||
foreach (var expectedMessage in expected)
|
||||
{
|
||||
actual.MoveNext();
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, actual.Current, new[] { "IdempotencyId" });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -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,50 @@ 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 IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();
|
||||
|
||||
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(_eventIntegrationPublisher)
|
||||
.SetDependency(IntegrationType.Webhook)
|
||||
.Create();
|
||||
}
|
||||
|
||||
private static IntegrationMessage<WebhookIntegrationConfigurationDetails> expectedMessage(string template)
|
||||
{
|
||||
return new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
|
||||
{
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
MessageId = "TestMessageId",
|
||||
Configuration = new WebhookIntegrationConfigurationDetails(_url),
|
||||
RenderedTemplate = template,
|
||||
RetryCount = 0,
|
||||
DelayUntilDate = null
|
||||
};
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
|
||||
{
|
||||
return [];
|
||||
@ -58,7 +76,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 +88,7 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
Assert.Empty(sutProvider.Sut.CapturedCalls);
|
||||
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -80,11 +98,13 @@ 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(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
}
|
||||
@ -100,10 +120,11 @@ 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(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty);
|
||||
}
|
||||
@ -118,10 +139,13 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(organization);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
Assert.Single(sutProvider.Sut.CapturedCalls);
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
|
||||
var expectedTemplate = $"Org: {organization.Name}";
|
||||
Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate);
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}");
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty);
|
||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
}
|
||||
@ -137,10 +161,11 @@ 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(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty);
|
||||
}
|
||||
@ -151,7 +176,7 @@ public class IntegrationEventHandlerBaseEventHandlerTests
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
Assert.Empty(sutProvider.Sut.CapturedCalls);
|
||||
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -161,15 +186,13 @@ 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 _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,39 +204,17 @@ 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 _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
|
||||
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 _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
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"),
|
||||
MessageId = "TestMessageId",
|
||||
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());
|
||||
}
|
||||
}
|
@ -22,9 +22,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;
|
||||
@ -63,7 +60,12 @@ public class OrganizationServiceTests
|
||||
existingUsers.First().Type = OrganizationUserType.Owner;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||
|
||||
@ -100,10 +102,6 @@ public class OrganizationServiceTests
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>(events =>
|
||||
events.Count() == expectedNewUsersCount));
|
||||
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
|
||||
referenceEvent.Type == ReferenceEventType.InvitedUsers && referenceEvent.Id == org.Id &&
|
||||
referenceEvent.Users == expectedNewUsersCount));
|
||||
}
|
||||
|
||||
[Theory, PaidOrganizationCustomize, BitAutoData]
|
||||
@ -124,7 +122,12 @@ public class OrganizationServiceTests
|
||||
ExternalId = reInvitedUser.Email,
|
||||
});
|
||||
var expectedNewUsersCount = newUsers.Count - 1;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id)
|
||||
.Returns(existingUsers);
|
||||
@ -170,10 +173,6 @@ public class OrganizationServiceTests
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>(events =>
|
||||
events.Count(e => e.Item2 == EventType.OrganizationUser_Invited) == expectedNewUsersCount));
|
||||
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
|
||||
referenceEvent.Type == ReferenceEventType.InvitedUsers && referenceEvent.Id == org.Id &&
|
||||
referenceEvent.Users == expectedNewUsersCount));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -201,7 +200,12 @@ public class OrganizationServiceTests
|
||||
sutProvider.Create();
|
||||
|
||||
invite.Emails = invite.Emails.Append(invite.Emails.First());
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
||||
@ -232,6 +236,12 @@ public class OrganizationServiceTests
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));
|
||||
Assert.Contains("Organization must have at least one confirmed owner.", exception.Message);
|
||||
@ -325,6 +335,12 @@ public class OrganizationServiceTests
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
|
||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||
|
||||
@ -351,6 +367,13 @@ public class OrganizationServiceTests
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var currentContext = sutProvider.GetDependency<ICurrentContext>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())
|
||||
@ -408,7 +431,12 @@ public class OrganizationServiceTests
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var currentContext = sutProvider.GetDependency<ICurrentContext>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
currentContext.OrganizationCustom(organization.Id).Returns(true);
|
||||
currentContext.ManageUsers(organization.Id).Returns(true);
|
||||
@ -436,7 +464,12 @@ public class OrganizationServiceTests
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||
|
||||
currentContext.OrganizationOwner(organization.Id).Returns(true);
|
||||
@ -484,7 +517,12 @@ public class OrganizationServiceTests
|
||||
|
||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
await sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId);
|
||||
|
||||
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)
|
||||
@ -549,7 +587,12 @@ public class OrganizationServiceTests
|
||||
|
||||
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut
|
||||
.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId));
|
||||
Assert.Contains("This user has already been invited", exception.Message);
|
||||
@ -606,7 +649,12 @@ public class OrganizationServiceTests
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(true);
|
||||
@ -642,7 +690,12 @@ public class OrganizationServiceTests
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var currentContext = sutProvider.GetDependency<ICurrentContext>();
|
||||
@ -675,6 +728,13 @@ public class OrganizationServiceTests
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
|
||||
// Set up some invites to grant access to SM
|
||||
invites.First().invite.AccessSecretsManager = true;
|
||||
var invitedSmUsers = invites.First().invite.Emails.Count();
|
||||
@ -719,6 +779,13 @@ public class OrganizationServiceTests
|
||||
invite.AccessSecretsManager = false;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
|
||||
// Assume we need to add seats for all invited SM users
|
||||
sutProvider.GetDependency<ICountNewSmSeatsRequiredQuery>()
|
||||
.CountNewSmSeatsRequiredAsync(organization.Id, invitedSmUsers).Returns(invitedSmUsers);
|
||||
@ -728,9 +795,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));
|
||||
@ -825,7 +891,12 @@ public class OrganizationServiceTests
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, null));
|
||||
|
@ -0,0 +1,173 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RabbitMqEventListenerServiceTests
|
||||
{
|
||||
private const string _queueName = "test_queue";
|
||||
private readonly IRabbitMqService _rabbitMqService = Substitute.For<IRabbitMqService>();
|
||||
private readonly ILogger<RabbitMqEventListenerService> _logger = Substitute.For<ILogger<RabbitMqEventListenerService>>();
|
||||
|
||||
private SutProvider<RabbitMqEventListenerService> GetSutProvider()
|
||||
{
|
||||
return new SutProvider<RabbitMqEventListenerService>()
|
||||
.SetDependency(_rabbitMqService)
|
||||
.SetDependency(_logger)
|
||||
.SetDependency(_queueName, "queueName")
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_CreatesQueue()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
await _rabbitMqService.Received(1).CreateEventQueueAsync(
|
||||
Arg.Is(_queueName),
|
||||
Arg.Is(cancellationToken)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessReceivedMessageAsync_EmptyJson_LogsError()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: new byte[0]);
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
|
||||
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<JsonException>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: JsonSerializer.SerializeToUtf8Bytes("{ Inavlid JSON"));
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
|
||||
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString().Contains("Invalid JSON")),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessReceivedMessageAsync_InvalidJsonArray_LogsError()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: JsonSerializer.SerializeToUtf8Bytes(new[] { "not a valid", "list of event messages" }));
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
|
||||
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<JsonException>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessReceivedMessageAsync_InvalidJsonObject_LogsError()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: JsonSerializer.SerializeToUtf8Bytes(DateTime.UtcNow)); // wrong object - not EventMessage
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
|
||||
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<JsonException>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessReceivedMessageAsync_SingleEvent_DelegatesToHandler(EventMessage message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: JsonSerializer.SerializeToUtf8Bytes(message));
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
|
||||
|
||||
await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleEventAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message, new[] { "IdempotencyId" })));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessReceivedMessageAsync_ManyEvents_DelegatesToHandler(IEnumerable<EventMessage> messages)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: JsonSerializer.SerializeToUtf8Bytes(messages));
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);
|
||||
|
||||
await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleManyEventsAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(messages, new[] { "IdempotencyId" })));
|
||||
}
|
||||
}
|
@ -0,0 +1,230 @@
|
||||
using System.Text;
|
||||
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 RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RabbitMqIntegrationListenerServiceTests
|
||||
{
|
||||
private const int _maxRetries = 3;
|
||||
private const string _queueName = "test_queue";
|
||||
private const string _retryQueueName = "test_queue_retry";
|
||||
private const string _routingKey = "test_routing_key";
|
||||
private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>();
|
||||
private readonly IRabbitMqService _rabbitMqService = Substitute.For<IRabbitMqService>();
|
||||
|
||||
private SutProvider<RabbitMqIntegrationListenerService> GetSutProvider()
|
||||
{
|
||||
return new SutProvider<RabbitMqIntegrationListenerService>()
|
||||
.SetDependency(_handler)
|
||||
.SetDependency(_rabbitMqService)
|
||||
.SetDependency(_queueName, "queueName")
|
||||
.SetDependency(_retryQueueName, "retryQueueName")
|
||||
.SetDependency(_routingKey, "routingKey")
|
||||
.SetDependency(_maxRetries, "maxRetries")
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_CreatesQueues()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
await _rabbitMqService.Received(1).CreateIntegrationQueuesAsync(
|
||||
Arg.Is(_queueName),
|
||||
Arg.Is(_retryQueueName),
|
||||
Arg.Is(_routingKey),
|
||||
Arg.Is(cancellationToken)
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessReceivedMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.RetryCount = 0;
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: Encoding.UTF8.GetBytes(message.ToJson())
|
||||
);
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = false;
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
|
||||
|
||||
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
|
||||
|
||||
await _rabbitMqService.Received(1).PublishToDeadLetterAsync(
|
||||
Arg.Any<IChannel>(),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.RepublishToRetryQueueAsync(default, default);
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.PublishToRetryAsync(default, default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessReceivedMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.RetryCount = _maxRetries;
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: Encoding.UTF8.GetBytes(message.ToJson())
|
||||
);
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
|
||||
|
||||
expected.ApplyRetry(result.DelayUntilDate);
|
||||
await _rabbitMqService.Received(1).PublishToDeadLetterAsync(
|
||||
Arg.Any<IChannel>(),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.RepublishToRetryQueueAsync(default, default);
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.PublishToRetryAsync(default, default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessReceivedMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
message.RetryCount = 0;
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: Encoding.UTF8.GetBytes(message.ToJson())
|
||||
);
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1);
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
|
||||
|
||||
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
|
||||
|
||||
expected.ApplyRetry(result.DelayUntilDate);
|
||||
await _rabbitMqService.Received(1).PublishToRetryAsync(
|
||||
Arg.Any<IChannel>(),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.RepublishToRetryQueueAsync(default, default);
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.PublishToDeadLetterAsync(default, default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessReceivedMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: Encoding.UTF8.GetBytes(message.ToJson())
|
||||
);
|
||||
var result = new IntegrationHandlerResult(true, message);
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
|
||||
|
||||
await _handler.Received(1).HandleAsync(Arg.Is(message.ToJson()));
|
||||
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.RepublishToRetryQueueAsync(default, default);
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.PublishToRetryAsync(default, default, default);
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.PublishToDeadLetterAsync(default, default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProcessReceivedMessageAsync_TooEarlyRetry_RepublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var cancellationToken = CancellationToken.None;
|
||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||
|
||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(1);
|
||||
var eventArgs = new BasicDeliverEventArgs(
|
||||
consumerTag: string.Empty,
|
||||
deliveryTag: 0,
|
||||
redelivered: true,
|
||||
exchange: string.Empty,
|
||||
routingKey: string.Empty,
|
||||
new BasicProperties(),
|
||||
body: Encoding.UTF8.GetBytes(message.ToJson())
|
||||
);
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
|
||||
|
||||
await _rabbitMqService.Received(1)
|
||||
.RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());
|
||||
|
||||
await _handler.DidNotReceiveWithAnyArgs().HandleAsync(default);
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.PublishToRetryAsync(default, default, default);
|
||||
await _rabbitMqService.DidNotReceiveWithAnyArgs()
|
||||
.PublishToDeadLetterAsync(default, default, default);
|
||||
}
|
||||
}
|
@ -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,4 +1,6 @@
|
||||
using System.Net;
|
||||
#nullable enable
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@ -257,10 +259,10 @@ public class SlackServiceTests
|
||||
public void GetRedirectUrl_ReturnsCorrectUrl()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var ClientId = sutProvider.GetDependency<GlobalSettings>().Slack.ClientId;
|
||||
var Scopes = sutProvider.GetDependency<GlobalSettings>().Slack.Scopes;
|
||||
var clientId = sutProvider.GetDependency<GlobalSettings>().Slack.ClientId;
|
||||
var scopes = sutProvider.GetDependency<GlobalSettings>().Slack.Scopes;
|
||||
var redirectUrl = "https://example.com/callback";
|
||||
var expectedUrl = $"https://slack.com/oauth/v2/authorize?client_id={ClientId}&scope={Scopes}&redirect_uri={redirectUrl}";
|
||||
var expectedUrl = $"https://slack.com/oauth/v2/authorize?client_id={clientId}&scope={scopes}&redirect_uri={redirectUrl}";
|
||||
var result = sutProvider.Sut.GetRedirectUrl(redirectUrl);
|
||||
Assert.Equal(expectedUrl, result);
|
||||
}
|
||||
|
@ -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,143 @@
|
||||
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));
|
||||
Assert.Equal("Too Many Requests", result.FailureReason);
|
||||
}
|
||||
|
||||
[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));
|
||||
Assert.Equal("Too Many Requests", result.FailureReason);
|
||||
}
|
||||
|
||||
[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);
|
||||
Assert.Equal("Internal Server Error", result.FailureReason);
|
||||
}
|
||||
|
||||
[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);
|
||||
Assert.Equal("Temporary Redirect", result.FailureReason);
|
||||
}
|
||||
}
|
@ -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]
|
||||
|
@ -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,18 +200,9 @@ 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>()
|
||||
.Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.SignupInitiationPath == default));
|
||||
|
||||
// Even if user doesn't have reference data, we should send them welcome email
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
@ -359,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]
|
||||
@ -429,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]
|
||||
@ -506,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]
|
||||
@ -604,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,6 +3,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@ -25,30 +26,32 @@ public class OrganizationBillingServiceTests
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().ListPlans().Returns(StaticStore.Plans.ToList());
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
var subscriberService = sutProvider.GetDependency<ISubscriberService>();
|
||||
|
||||
subscriberService
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options => options.Expand.FirstOrDefault() == "discount.coupon.applies_to"))
|
||||
.Returns(new Customer
|
||||
var organizationSeatCount = new OrganizationSeatCounts { Users = 1, Sponsored = 0 };
|
||||
var customer = new Customer
|
||||
{
|
||||
Discount = new Discount
|
||||
{
|
||||
Discount = new Discount
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Coupon = new Coupon
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = ["product_id"]
|
||||
}
|
||||
Products = ["product_id"]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
subscriberService
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.Returns(customer);
|
||||
|
||||
subscriberService.GetSubscription(organization).Returns(new Subscription
|
||||
{
|
||||
@ -67,6 +70,10 @@ public class OrganizationBillingServiceTests
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Users = 1, Sponsored = 0 });
|
||||
|
||||
var metadata = await sutProvider.Sut.GetMetadata(organizationId);
|
||||
|
||||
Assert.True(metadata!.IsOnSecretsManagerStandalone);
|
||||
|
@ -1,17 +1,17 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Dirt.Reports.Entities;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Dirt.Reports.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.ReportFeatures;
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AddPasswordHealthReportApplicationCommandTests
|
||||
|
@ -1,15 +1,15 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Dirt.Reports.Entities;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Dirt.Reports.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.ReportFeatures;
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DeletePasswordHealthReportApplicationCommandTests
|
||||
|
@ -1,14 +1,14 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Dirt.Reports.Entities;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Reports.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.ReportFeatures;
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetPasswordHealthReportApplicationQueryTests
|
||||
|
@ -5,6 +5,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -169,9 +170,13 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(sponsoringOrgUser.UserId.Value);
|
||||
|
||||
// Setup for checking available seats
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
|
||||
.Returns(0);
|
||||
.Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 0
|
||||
});
|
||||
|
||||
|
||||
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||
@ -318,9 +323,13 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
]);
|
||||
|
||||
// Setup for checking available seats - organization has plenty of seats
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
|
||||
.Returns(5);
|
||||
.Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 5
|
||||
});
|
||||
|
||||
var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes);
|
||||
@ -378,9 +387,13 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
]);
|
||||
|
||||
// Setup for checking available seats - organization has no available seats
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
|
||||
.Returns(10);
|
||||
.Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 10
|
||||
});
|
||||
|
||||
// Setup for checking if can scale
|
||||
sutProvider.GetDependency<IOrganizationService>()
|
||||
@ -443,9 +456,13 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
]);
|
||||
|
||||
// Setup for checking available seats - organization has no available seats
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)
|
||||
.Returns(10);
|
||||
.Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 10
|
||||
});
|
||||
|
||||
// Setup for checking if can scale - cannot scale
|
||||
var failureReason = "Seat limit has been reached.";
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
@ -77,6 +78,12 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
upgrade.AdditionalSeats = 10;
|
||||
upgrade.Plan = PlanType.TeamsAnnually;
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);
|
||||
}
|
||||
@ -107,7 +114,12 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
organizationUpgrade.Plan = planType;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organizationUpgrade.Plan).Returns(StaticStore.GetPlan(organizationUpgrade.Plan));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade);
|
||||
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSubscription(
|
||||
organization,
|
||||
@ -141,7 +153,12 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
upgrade.AdditionalSeats = 15;
|
||||
upgrade.AdditionalSmSeats = 10;
|
||||
upgrade.AdditionalServiceAccounts = 20;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
var result = await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(
|
||||
@ -173,6 +190,12 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(2);
|
||||
|
||||
@ -202,6 +225,12 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(1);
|
||||
sutProvider.GetDependency<IServiceAccountRepository>()
|
||||
|
@ -2,15 +2,15 @@
|
||||
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.Repositories;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -18,23 +18,15 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
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;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Fakes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@ -177,9 +169,12 @@ public class UserServiceTests
|
||||
[Theory]
|
||||
[BitAutoData(DeviceType.UnknownBrowser, "Unknown Browser")]
|
||||
[BitAutoData(DeviceType.Android, "Android")]
|
||||
public async Task SendNewDeviceVerificationEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName, SutProvider<UserService> sutProvider, User user)
|
||||
public async Task SendNewDeviceVerificationEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName,
|
||||
User user)
|
||||
{
|
||||
SetupFakeTokenProvider(sutProvider, user);
|
||||
var sutProvider = new SutProvider<UserService>()
|
||||
.CreateWithUserServiceCustomizations(user);
|
||||
|
||||
var context = sutProvider.GetDependency<ICurrentContext>();
|
||||
context.DeviceType = deviceType;
|
||||
context.IpAddress = "1.1.1.1";
|
||||
@ -192,9 +187,11 @@ public class UserServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SendNewDeviceVerificationEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(SutProvider<UserService> sutProvider, User user)
|
||||
public async Task SendNewDeviceVerificationEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(User user)
|
||||
{
|
||||
SetupFakeTokenProvider(sutProvider, user);
|
||||
var sutProvider = new SutProvider<UserService>()
|
||||
.CreateWithUserServiceCustomizations(user);
|
||||
|
||||
var context = sutProvider.GetDependency<ICurrentContext>();
|
||||
context.DeviceType = null;
|
||||
context.IpAddress = "1.1.1.1";
|
||||
@ -264,76 +261,28 @@ public class UserServiceTests
|
||||
[BitAutoData(true, "bad_test_password", false, ShouldCheck.Password | ShouldCheck.OTP)]
|
||||
public async Task VerifySecretAsync_Works(
|
||||
bool shouldHavePassword, string secret, bool expectedIsVerified, ShouldCheck shouldCheck, // inline theory data
|
||||
SutProvider<UserService> sutProvider, User user) // AutoFixture injected data
|
||||
User user) // AutoFixture injected data
|
||||
{
|
||||
// Arrange
|
||||
var tokenProvider = SetupFakeTokenProvider(sutProvider, user);
|
||||
SetupUserAndDevice(user, shouldHavePassword);
|
||||
|
||||
var sutProvider = new SutProvider<UserService>()
|
||||
.CreateWithUserServiceCustomizations(user);
|
||||
|
||||
// Setup the fake password verification
|
||||
var substitutedUserPasswordStore = Substitute.For<IUserPasswordStore<User>>();
|
||||
substitutedUserPasswordStore
|
||||
sutProvider.GetDependency<IUserPasswordStore<User>>()
|
||||
.GetPasswordHashAsync(user, Arg.Any<CancellationToken>())
|
||||
.Returns((ci) =>
|
||||
{
|
||||
return Task.FromResult("hashed_test_password");
|
||||
});
|
||||
.Returns(Task.FromResult("hashed_test_password"));
|
||||
|
||||
sutProvider.SetDependency<IUserStore<User>>(substitutedUserPasswordStore, "store");
|
||||
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>("passwordHasher")
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>()
|
||||
.VerifyHashedPassword(user, "hashed_test_password", "test_password")
|
||||
.Returns((ci) =>
|
||||
{
|
||||
return PasswordVerificationResult.Success;
|
||||
});
|
||||
.Returns(PasswordVerificationResult.Success);
|
||||
|
||||
// HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured
|
||||
var sut = new UserService(
|
||||
sutProvider.GetDependency<IUserRepository>(),
|
||||
sutProvider.GetDependency<ICipherRepository>(),
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>(),
|
||||
sutProvider.GetDependency<IOrganizationRepository>(),
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>(),
|
||||
sutProvider.GetDependency<IMailService>(),
|
||||
sutProvider.GetDependency<IPushNotificationService>(),
|
||||
sutProvider.GetDependency<IUserStore<User>>(),
|
||||
sutProvider.GetDependency<IOptions<IdentityOptions>>(),
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>(),
|
||||
sutProvider.GetDependency<IEnumerable<IUserValidator<User>>>(),
|
||||
sutProvider.GetDependency<IEnumerable<IPasswordValidator<User>>>(),
|
||||
sutProvider.GetDependency<ILookupNormalizer>(),
|
||||
sutProvider.GetDependency<IdentityErrorDescriber>(),
|
||||
sutProvider.GetDependency<IServiceProvider>(),
|
||||
sutProvider.GetDependency<ILogger<UserManager<User>>>(),
|
||||
sutProvider.GetDependency<ILicensingService>(),
|
||||
sutProvider.GetDependency<IEventService>(),
|
||||
sutProvider.GetDependency<IApplicationCacheService>(),
|
||||
sutProvider.GetDependency<IDataProtectionProvider>(),
|
||||
sutProvider.GetDependency<IPaymentService>(),
|
||||
sutProvider.GetDependency<IPolicyRepository>(),
|
||||
sutProvider.GetDependency<IPolicyService>(),
|
||||
sutProvider.GetDependency<IReferenceEventService>(),
|
||||
sutProvider.GetDependency<IFido2>(),
|
||||
sutProvider.GetDependency<ICurrentContext>(),
|
||||
sutProvider.GetDependency<IGlobalSettings>(),
|
||||
sutProvider.GetDependency<IAcceptOrgUserCommand>(),
|
||||
sutProvider.GetDependency<IProviderUserRepository>(),
|
||||
sutProvider.GetDependency<IStripeSyncService>(),
|
||||
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
|
||||
sutProvider.GetDependency<IFeatureService>(),
|
||||
sutProvider.GetDependency<IPremiumUserBillingService>(),
|
||||
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
|
||||
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
|
||||
sutProvider.GetDependency<IDistributedCache>()
|
||||
);
|
||||
|
||||
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
|
||||
var actualIsVerified = await sutProvider.Sut.VerifySecretAsync(user, secret);
|
||||
|
||||
Assert.Equal(expectedIsVerified, actualIsVerified);
|
||||
|
||||
await tokenProvider
|
||||
await sutProvider.GetDependency<IUserTwoFactorTokenProvider<User>>()
|
||||
.Received(shouldCheck.HasFlag(ShouldCheck.OTP) ? 1 : 0)
|
||||
.ValidateAsync(Arg.Any<string>(), secret, Arg.Any<UserManager<User>>(), user);
|
||||
|
||||
@ -462,6 +411,78 @@ public class UserServiceTests
|
||||
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
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)
|
||||
@ -509,6 +530,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)
|
||||
@ -540,26 +608,25 @@ public class UserServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ResendNewDeviceVerificationEmail_SendsToken_Success(
|
||||
SutProvider<UserService> sutProvider, User user)
|
||||
public async Task ResendNewDeviceVerificationEmail_SendsToken_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
var testPassword = "test_password";
|
||||
var tokenProvider = SetupFakeTokenProvider(sutProvider, user);
|
||||
SetupUserAndDevice(user, true);
|
||||
|
||||
var sutProvider = new SutProvider<UserService>()
|
||||
.CreateWithUserServiceCustomizations(user);
|
||||
|
||||
// Setup the fake password verification
|
||||
var substitutedUserPasswordStore = Substitute.For<IUserPasswordStore<User>>();
|
||||
substitutedUserPasswordStore
|
||||
sutProvider
|
||||
.GetDependency<IUserPasswordStore<User>>()
|
||||
.GetPasswordHashAsync(user, Arg.Any<CancellationToken>())
|
||||
.Returns((ci) =>
|
||||
{
|
||||
return Task.FromResult("hashed_test_password");
|
||||
});
|
||||
|
||||
sutProvider.SetDependency<IUserStore<User>>(substitutedUserPasswordStore, "store");
|
||||
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>("passwordHasher")
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>()
|
||||
.VerifyHashedPassword(user, "hashed_test_password", testPassword)
|
||||
.Returns((ci) =>
|
||||
{
|
||||
@ -574,10 +641,7 @@ public class UserServiceTests
|
||||
context.DeviceType = DeviceType.Android;
|
||||
context.IpAddress = "1.1.1.1";
|
||||
|
||||
// HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured
|
||||
var sut = RebuildSut(sutProvider);
|
||||
|
||||
await sut.ResendNewDeviceVerificationEmail(user.Email, testPassword);
|
||||
await sutProvider.Sut.ResendNewDeviceVerificationEmail(user.Email, testPassword);
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
@ -721,8 +785,15 @@ public class UserServiceTests
|
||||
user.MasterPassword = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IUserTwoFactorTokenProvider<User> SetupFakeTokenProvider(SutProvider<UserService> sutProvider, User user)
|
||||
public static class UserServiceSutProviderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Arranges a fake token provider. Must call as part of a builder pattern that ends in Create(), as it modifies
|
||||
/// the SutProvider build chain.
|
||||
/// </summary>
|
||||
private static SutProvider<UserService> SetFakeTokenProvider(this SutProvider<UserService> sutProvider, User user)
|
||||
{
|
||||
var fakeUserTwoFactorProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();
|
||||
|
||||
@ -738,8 +809,11 @@ public class UserServiceTests
|
||||
.ValidateAsync(Arg.Any<string>(), "otp_token", Arg.Any<UserManager<User>>(), user)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOptions<IdentityOptions>>()
|
||||
.Value.Returns(new IdentityOptions
|
||||
var fakeIdentityOptions = Substitute.For<IOptions<IdentityOptions>>();
|
||||
|
||||
fakeIdentityOptions
|
||||
.Value
|
||||
.Returns(new IdentityOptions
|
||||
{
|
||||
Tokens = new TokenOptions
|
||||
{
|
||||
@ -753,54 +827,54 @@ public class UserServiceTests
|
||||
}
|
||||
});
|
||||
|
||||
// The above arranging of dependencies is used in the constructor of UserManager
|
||||
// ref: https://github.com/dotnet/aspnetcore/blob/bfeb3bf9005c36b081d1e48725531ee0e15a9dfb/src/Identity/Extensions.Core/src/UserManager.cs#L103-L120
|
||||
// since the constructor of the Sut has ran already (when injected) I need to recreate it to get it to run again
|
||||
sutProvider.Create();
|
||||
sutProvider.SetDependency(fakeIdentityOptions);
|
||||
// Also set the fake provider dependency so that we can retrieve it easily via GetDependency
|
||||
sutProvider.SetDependency(fakeUserTwoFactorProvider);
|
||||
|
||||
return fakeUserTwoFactorProvider;
|
||||
return sutProvider;
|
||||
}
|
||||
|
||||
private IUserService RebuildSut(SutProvider<UserService> sutProvider)
|
||||
/// <summary>
|
||||
/// Properly registers IUserPasswordStore as IUserStore so it's injected when the sut is initialized.
|
||||
/// </summary>
|
||||
/// <param name="sutProvider"></param>
|
||||
/// <returns></returns>
|
||||
private static SutProvider<UserService> SetUserPasswordStore(this SutProvider<UserService> sutProvider)
|
||||
{
|
||||
return new UserService(
|
||||
sutProvider.GetDependency<IUserRepository>(),
|
||||
sutProvider.GetDependency<ICipherRepository>(),
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>(),
|
||||
sutProvider.GetDependency<IOrganizationRepository>(),
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>(),
|
||||
sutProvider.GetDependency<IMailService>(),
|
||||
sutProvider.GetDependency<IPushNotificationService>(),
|
||||
sutProvider.GetDependency<IUserStore<User>>(),
|
||||
sutProvider.GetDependency<IOptions<IdentityOptions>>(),
|
||||
sutProvider.GetDependency<IPasswordHasher<User>>(),
|
||||
sutProvider.GetDependency<IEnumerable<IUserValidator<User>>>(),
|
||||
sutProvider.GetDependency<IEnumerable<IPasswordValidator<User>>>(),
|
||||
sutProvider.GetDependency<ILookupNormalizer>(),
|
||||
sutProvider.GetDependency<IdentityErrorDescriber>(),
|
||||
sutProvider.GetDependency<IServiceProvider>(),
|
||||
sutProvider.GetDependency<ILogger<UserManager<User>>>(),
|
||||
sutProvider.GetDependency<ILicensingService>(),
|
||||
sutProvider.GetDependency<IEventService>(),
|
||||
sutProvider.GetDependency<IApplicationCacheService>(),
|
||||
sutProvider.GetDependency<IDataProtectionProvider>(),
|
||||
sutProvider.GetDependency<IPaymentService>(),
|
||||
sutProvider.GetDependency<IPolicyRepository>(),
|
||||
sutProvider.GetDependency<IPolicyService>(),
|
||||
sutProvider.GetDependency<IReferenceEventService>(),
|
||||
sutProvider.GetDependency<IFido2>(),
|
||||
sutProvider.GetDependency<ICurrentContext>(),
|
||||
sutProvider.GetDependency<IGlobalSettings>(),
|
||||
sutProvider.GetDependency<IAcceptOrgUserCommand>(),
|
||||
sutProvider.GetDependency<IProviderUserRepository>(),
|
||||
sutProvider.GetDependency<IStripeSyncService>(),
|
||||
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
|
||||
sutProvider.GetDependency<IFeatureService>(),
|
||||
sutProvider.GetDependency<IPremiumUserBillingService>(),
|
||||
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
|
||||
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
|
||||
sutProvider.GetDependency<IDistributedCache>()
|
||||
);
|
||||
var substitutedUserPasswordStore = Substitute.For<IUserPasswordStore<User>>();
|
||||
|
||||
// IUserPasswordStore must be registered under the IUserStore parameter to be properly injected
|
||||
// because this is what the constructor expects
|
||||
sutProvider.SetDependency<IUserStore<User>>(substitutedUserPasswordStore);
|
||||
|
||||
// Also store it under its own type for retrieval and configuration
|
||||
sutProvider.SetDependency(substitutedUserPasswordStore);
|
||||
|
||||
return sutProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a hack: when autofixture initializes the sut in sutProvider, it overwrites the public
|
||||
/// PasswordHasher property with a new substitute, so it loses the configured sutProvider mock.
|
||||
/// This doesn't usually happen because our dependencies are not usually public.
|
||||
/// Call this AFTER SutProvider.Create().
|
||||
/// </summary>
|
||||
private static SutProvider<UserService> FixPasswordHasherBug(this SutProvider<UserService> sutProvider)
|
||||
{
|
||||
// Get the configured sutProvider mock and assign it back to the public property in the base class
|
||||
sutProvider.Sut.PasswordHasher = sutProvider.GetDependency<IPasswordHasher<User>>();
|
||||
return sutProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A helper that combines all SutProvider configuration usually required for UserService.
|
||||
/// Call this instead of SutProvider.Create, after any additional configuration your test needs.
|
||||
/// </summary>
|
||||
public static SutProvider<UserService> CreateWithUserServiceCustomizations(this SutProvider<UserService> sutProvider, User user)
|
||||
=> sutProvider
|
||||
.SetUserPasswordStore()
|
||||
.SetFakeTokenProvider(user)
|
||||
.Create()
|
||||
.FixPasswordHasherBug();
|
||||
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -8,7 +8,6 @@ using Bit.Core.Test.AutoFixture.CurrentContextFixtures;
|
||||
using Bit.Core.Test.Tools.AutoFixture.SendFixtures;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.SendFeatures;
|
||||
@ -32,7 +31,6 @@ public class NonAnonymousSendCommandTests
|
||||
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||
private readonly ISendValidationService _sendValidationService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISendCoreHelperService _sendCoreHelperService;
|
||||
private readonly NonAnonymousSendCommand _nonAnonymousSendCommand;
|
||||
@ -45,7 +43,6 @@ public class NonAnonymousSendCommandTests
|
||||
_sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_sendValidationService = Substitute.For<ISendValidationService>();
|
||||
_referenceEventService = Substitute.For<IReferenceEventService>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
_sendCoreHelperService = Substitute.For<ISendCoreHelperService>();
|
||||
|
||||
@ -55,8 +52,6 @@ public class NonAnonymousSendCommandTests
|
||||
_pushNotificationService,
|
||||
_sendAuthorizationService,
|
||||
_sendValidationService,
|
||||
_referenceEventService,
|
||||
_currentContext,
|
||||
_sendCoreHelperService
|
||||
);
|
||||
}
|
||||
@ -135,14 +130,6 @@ public class NonAnonymousSendCommandTests
|
||||
// For new Sends
|
||||
await _sendRepository.Received(1).CreateAsync(send);
|
||||
await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send);
|
||||
await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(e =>
|
||||
e.Id == userId &&
|
||||
e.Type == ReferenceEventType.SendCreated &&
|
||||
e.Source == ReferenceEventSource.User &&
|
||||
e.SendType == send.Type &&
|
||||
e.SendHasNotes == true &&
|
||||
e.ClientId == "test-client" &&
|
||||
e.ClientVersion == Version.Parse("1.0.0")));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -150,7 +137,6 @@ public class NonAnonymousSendCommandTests
|
||||
await _sendRepository.Received(1).UpsertAsync(send);
|
||||
Assert.NotEqual(initialDate, send.RevisionDate);
|
||||
await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);
|
||||
await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,14 +220,6 @@ public class NonAnonymousSendCommandTests
|
||||
// For new Sends
|
||||
await _sendRepository.Received(1).CreateAsync(send);
|
||||
await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send);
|
||||
await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(e =>
|
||||
e.Id == userId &&
|
||||
e.Type == ReferenceEventType.SendCreated &&
|
||||
e.Source == ReferenceEventSource.User &&
|
||||
e.SendType == send.Type &&
|
||||
e.HasPassword == false &&
|
||||
e.ClientId == "test-client" &&
|
||||
e.ClientVersion == Version.Parse("1.0.0")));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -249,7 +227,6 @@ public class NonAnonymousSendCommandTests
|
||||
await _sendRepository.Received(1).UpsertAsync(send);
|
||||
Assert.NotEqual(initialDate, send.RevisionDate);
|
||||
await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);
|
||||
await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,7 +262,6 @@ public class NonAnonymousSendCommandTests
|
||||
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
|
||||
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
|
||||
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
|
||||
await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -328,14 +304,6 @@ public class NonAnonymousSendCommandTests
|
||||
// For new Sends
|
||||
await _sendRepository.Received(1).CreateAsync(send);
|
||||
await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send);
|
||||
await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(e =>
|
||||
e.Id == userId &&
|
||||
e.Type == ReferenceEventType.SendCreated &&
|
||||
e.Source == ReferenceEventSource.User &&
|
||||
e.SendType == send.Type &&
|
||||
e.SendHasNotes == true &&
|
||||
e.ClientId == "test-client" &&
|
||||
e.ClientVersion == Version.Parse("1.0.0")));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -343,7 +311,6 @@ public class NonAnonymousSendCommandTests
|
||||
await _sendRepository.Received(1).UpsertAsync(send);
|
||||
Assert.NotEqual(initialDate, send.RevisionDate);
|
||||
await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);
|
||||
await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
||||
}
|
||||
}
|
||||
|
||||
@ -386,9 +353,6 @@ public class NonAnonymousSendCommandTests
|
||||
// Verify push notification wasn't sent
|
||||
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
|
||||
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
|
||||
|
||||
// Verify reference event service wasn't called
|
||||
await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -431,13 +395,6 @@ public class NonAnonymousSendCommandTests
|
||||
// For new Sends
|
||||
await _sendRepository.Received(1).CreateAsync(send);
|
||||
await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send);
|
||||
await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is<ReferenceEvent>(e =>
|
||||
e.Id == userId &&
|
||||
e.Type == ReferenceEventType.SendCreated &&
|
||||
e.Source == ReferenceEventSource.User &&
|
||||
e.SendType == send.Type &&
|
||||
e.ClientId == "test-client" &&
|
||||
e.ClientVersion == Version.Parse("1.0.0")));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -445,7 +402,6 @@ public class NonAnonymousSendCommandTests
|
||||
await _sendRepository.Received(1).UpsertAsync(send);
|
||||
Assert.NotEqual(initialDate, send.RevisionDate);
|
||||
await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);
|
||||
await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
||||
}
|
||||
}
|
||||
|
||||
@ -481,9 +437,6 @@ public class NonAnonymousSendCommandTests
|
||||
|
||||
// Verify push notification was sent for the update
|
||||
await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);
|
||||
|
||||
// Verify no reference event was raised (only happens for new sends)
|
||||
await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
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
|
||||
};
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
@ -15,8 +14,6 @@ public class SendAuthorizationServiceTests
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly IPasswordHasher<Bit.Core.Entities.User> _passwordHasher;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly SendAuthorizationService _sendAuthorizationService;
|
||||
|
||||
public SendAuthorizationServiceTests()
|
||||
@ -24,15 +21,11 @@ public class SendAuthorizationServiceTests
|
||||
_sendRepository = Substitute.For<ISendRepository>();
|
||||
_passwordHasher = Substitute.For<IPasswordHasher<Bit.Core.Entities.User>>();
|
||||
_pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||
_referenceEventService = Substitute.For<IReferenceEventService>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
|
||||
_sendAuthorizationService = new SendAuthorizationService(
|
||||
_sendRepository,
|
||||
_passwordHasher,
|
||||
_pushNotificationService,
|
||||
_referenceEventService,
|
||||
_currentContext);
|
||||
_pushNotificationService);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ public class CipherServiceTests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ShareManyAsync_WrongRevisionDate_Throws(SutProvider<CipherService> sutProvider,
|
||||
IEnumerable<Cipher> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
|
||||
.Returns(new Organization
|
||||
@ -651,7 +651,7 @@ public class CipherServiceTests
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("Correct Time")]
|
||||
public async Task ShareManyAsync_CorrectRevisionDate_Passes(string revisionDateString,
|
||||
SutProvider<CipherService> sutProvider, IEnumerable<Cipher> ciphers, Organization organization, List<Guid> collectionIds)
|
||||
SutProvider<CipherService> sutProvider, IEnumerable<CipherDetails> ciphers, Organization organization, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id)
|
||||
.Returns(new Organization
|
||||
@ -673,13 +673,21 @@ public class CipherServiceTests
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_UpdatesUserCipher(Guid restoringUserId, CipherDetails cipher, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>().GetCanEditByIdAsync(restoringUserId, cipher.Id).Returns(true);
|
||||
cipher.UserId = restoringUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
var initialRevisionDate = new DateTime(1970, 1, 1, 0, 0, 0);
|
||||
cipher.DeletedDate = initialRevisionDate;
|
||||
cipher.RevisionDate = initialRevisionDate;
|
||||
|
||||
await sutProvider.Sut.RestoreAsync(cipher, restoringUserId, cipher.OrganizationId.HasValue);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(restoringUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = restoringUserId,
|
||||
});
|
||||
|
||||
await sutProvider.Sut.RestoreAsync(cipher, restoringUserId);
|
||||
|
||||
Assert.Null(cipher.DeletedDate);
|
||||
Assert.NotEqual(initialRevisionDate, cipher.RevisionDate);
|
||||
@ -688,15 +696,28 @@ public class CipherServiceTests
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_UpdatesOrganizationCipher(Guid restoringUserId, CipherDetails cipher, SutProvider<CipherService> sutProvider)
|
||||
public async Task RestoreAsync_UpdatesOrganizationCipher(Guid restoringUserId, CipherDetails cipher, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>().GetCanEditByIdAsync(restoringUserId, cipher.Id).Returns(true);
|
||||
cipher.OrganizationId = Guid.NewGuid();
|
||||
cipher.Edit = false;
|
||||
cipher.Manage = true;
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(restoringUserId)
|
||||
.Returns(user);
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(cipher.OrganizationId.Value)
|
||||
.Returns(new OrganizationAbility
|
||||
{
|
||||
Id = cipher.OrganizationId.Value,
|
||||
LimitItemDeletion = true
|
||||
});
|
||||
|
||||
var initialRevisionDate = new DateTime(1970, 1, 1, 0, 0, 0);
|
||||
cipher.DeletedDate = initialRevisionDate;
|
||||
cipher.RevisionDate = initialRevisionDate;
|
||||
|
||||
await sutProvider.Sut.RestoreAsync(cipher, restoringUserId, cipher.OrganizationId.HasValue);
|
||||
await sutProvider.Sut.RestoreAsync(cipher, restoringUserId);
|
||||
|
||||
Assert.Null(cipher.DeletedDate);
|
||||
Assert.NotEqual(initialRevisionDate, cipher.RevisionDate);
|
||||
@ -724,24 +745,12 @@ public class CipherServiceTests
|
||||
cipherDetails.UserId = Guid.NewGuid();
|
||||
cipherDetails.OrganizationId = null;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreAsync(cipherDetails, restoringUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().UpsertAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
|
||||
Guid restoringUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(restoringUserId, cipherDetails.Id)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(restoringUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = restoringUserId,
|
||||
});
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreAsync(cipherDetails, restoringUserId));
|
||||
@ -752,28 +761,6 @@ public class CipherServiceTests
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithEditPermission_RestoresCipherDetails(
|
||||
Guid restoringUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(restoringUserId, cipherDetails.Id)
|
||||
.Returns(true);
|
||||
|
||||
var initialRevisionDate = new DateTime(1970, 1, 1, 0, 0, 0);
|
||||
cipherDetails.DeletedDate = initialRevisionDate;
|
||||
cipherDetails.RevisionDate = initialRevisionDate;
|
||||
|
||||
await sutProvider.Sut.RestoreAsync(cipherDetails, restoringUserId);
|
||||
|
||||
Assert.Null(cipherDetails.DeletedDate);
|
||||
Assert.NotEqual(initialRevisionDate, cipherDetails.RevisionDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipherDetails);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipherDetails, EventType.Cipher_Restored);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipherDetails, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
@ -794,7 +781,7 @@ public class CipherServiceTests
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithLimitItemDeletionEnabled_WithManagePermission_RestoresCipher(
|
||||
public async Task RestoreAsync_WithManagePermission_RestoresCipher(
|
||||
Guid restoringUserId, CipherDetails cipherDetails, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipherDetails.OrganizationId = Guid.NewGuid();
|
||||
@ -802,9 +789,6 @@ public class CipherServiceTests
|
||||
cipherDetails.Edit = false;
|
||||
cipherDetails.Manage = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(restoringUserId)
|
||||
.Returns(user);
|
||||
@ -828,7 +812,7 @@ public class CipherServiceTests
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_WithLimitItemDeletionEnabled_WithoutManagePermission_ThrowsBadRequestException(
|
||||
public async Task RestoreAsync_WithoutManagePermission_ThrowsBadRequestException(
|
||||
Guid restoringUserId, CipherDetails cipherDetails, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipherDetails.OrganizationId = Guid.NewGuid();
|
||||
@ -836,9 +820,6 @@ public class CipherServiceTests
|
||||
cipherDetails.Edit = true;
|
||||
cipherDetails.Manage = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(restoringUserId)
|
||||
.Returns(user);
|
||||
@ -859,32 +840,7 @@ public class CipherServiceTests
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreManyAsync_UpdatesCiphers(ICollection<CipherDetails> ciphers,
|
||||
SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var cipherIds = ciphers.Select(c => c.Id).ToArray();
|
||||
var restoringUserId = ciphers.First().UserId.Value;
|
||||
var previousRevisionDate = DateTime.UtcNow;
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
cipher.Edit = true;
|
||||
cipher.RevisionDate = previousRevisionDate;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>().GetManyByUserIdAsync(restoringUserId).Returns(ciphers);
|
||||
var revisionDate = previousRevisionDate + TimeSpan.FromMinutes(1);
|
||||
sutProvider.GetDependency<ICipherRepository>().RestoreAsync(Arg.Any<IEnumerable<Guid>>(), restoringUserId).Returns(revisionDate);
|
||||
|
||||
await sutProvider.Sut.RestoreManyAsync(cipherIds, restoringUserId);
|
||||
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
Assert.Null(cipher.DeletedDate);
|
||||
Assert.Equal(revisionDate, cipher.RevisionDate);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
@ -971,90 +927,14 @@ public class CipherServiceTests
|
||||
.PushSyncCiphersAsync(restoringUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task RestoreManyAsync_WithOrgCipherAndEditPermission_RestoresCiphers(
|
||||
Guid restoringUserId, List<CipherDetails> ciphers, Guid organizationId, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var cipherIds = ciphers.Select(c => c.Id).ToArray();
|
||||
var previousRevisionDate = DateTime.UtcNow;
|
||||
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
cipher.OrganizationId = organizationId;
|
||||
cipher.Edit = true;
|
||||
cipher.DeletedDate = DateTime.UtcNow;
|
||||
cipher.RevisionDate = previousRevisionDate;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(restoringUserId)
|
||||
.Returns(ciphers);
|
||||
|
||||
var revisionDate = previousRevisionDate + TimeSpan.FromMinutes(1);
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.RestoreAsync(Arg.Any<IEnumerable<Guid>>(), restoringUserId)
|
||||
.Returns(revisionDate);
|
||||
|
||||
var result = await sutProvider.Sut.RestoreManyAsync(cipherIds, restoringUserId);
|
||||
|
||||
Assert.Equal(ciphers.Count, result.Count);
|
||||
foreach (var cipher in result)
|
||||
{
|
||||
Assert.Null(cipher.DeletedDate);
|
||||
Assert.Equal(revisionDate, cipher.RevisionDate);
|
||||
}
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == cipherIds.Count() &&
|
||||
ids.All(id => cipherIds.Contains(id))), restoringUserId);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogCipherEventsAsync(Arg.Any<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncCiphersAsync(restoringUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task RestoreManyAsync_WithOrgCipherLackingEditPermission_DoesNotRestoreCiphers(
|
||||
Guid restoringUserId, List<Cipher> ciphers, Guid organizationId, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var cipherIds = ciphers.Select(c => c.Id).ToArray();
|
||||
var cipherDetailsList = ciphers.Select(c => new CipherDetails
|
||||
{
|
||||
Id = c.Id,
|
||||
OrganizationId = organizationId,
|
||||
Edit = false,
|
||||
DeletedDate = DateTime.UtcNow
|
||||
}).ToList();
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(restoringUserId)
|
||||
.Returns(cipherDetailsList);
|
||||
|
||||
var result = await sutProvider.Sut.RestoreManyAsync(cipherIds, restoringUserId);
|
||||
|
||||
Assert.Empty(result);
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()), restoringUserId);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogCipherEventsAsync(Arg.Any<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncCiphersAsync(restoringUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task RestoreManyAsync_WithLimitItemDeletionEnabled_WithManagePermission_RestoresCiphers(
|
||||
public async Task RestoreManyAsync_WithManagePermission_RestoresCiphers(
|
||||
Guid restoringUserId, List<CipherDetails> ciphers, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
@ -1070,9 +950,6 @@ public class CipherServiceTests
|
||||
cipher.RevisionDate = previousRevisionDate;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(restoringUserId)
|
||||
.Returns(ciphers);
|
||||
@ -1121,7 +998,7 @@ public class CipherServiceTests
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task RestoreManyAsync_WithLimitItemDeletionEnabled_WithoutManagePermission_DoesNotRestoreCiphers(
|
||||
public async Task RestoreManyAsync_WithoutManagePermission_DoesNotRestoreCiphers(
|
||||
Guid restoringUserId, List<CipherDetails> ciphers, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
@ -1135,9 +1012,6 @@ public class CipherServiceTests
|
||||
cipher.DeletedDate = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(restoringUserId)
|
||||
.Returns(ciphers);
|
||||
@ -1173,7 +1047,7 @@ public class CipherServiceTests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ShareManyAsync_FreeOrgWithAttachment_Throws(SutProvider<CipherService> sutProvider,
|
||||
IEnumerable<Cipher> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(new Organization
|
||||
{
|
||||
@ -1194,7 +1068,7 @@ public class CipherServiceTests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ShareManyAsync_PaidOrgWithAttachment_Passes(SutProvider<CipherService> sutProvider,
|
||||
IEnumerable<Cipher> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
|
||||
.Returns(new Organization
|
||||
@ -1502,23 +1376,12 @@ public class CipherServiceTests
|
||||
cipherDetails.UserId = deletingUserId;
|
||||
cipherDetails.OrganizationId = null;
|
||||
|
||||
await sutProvider.Sut.DeleteAsync(cipherDetails, deletingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).DeleteAsync(cipherDetails);
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1).DeleteAttachmentsForCipherAsync(cipherDetails.Id);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipherDetails, EventType.Cipher_Deleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherDeleteAsync(cipherDetails);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithOrgCipherAndEditPermission_DeletesCipher(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipherDetails.Id)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = deletingUserId,
|
||||
});
|
||||
|
||||
await sutProvider.Sut.DeleteAsync(cipherDetails, deletingUserId);
|
||||
|
||||
@ -1528,6 +1391,8 @@ public class CipherServiceTests
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherDeleteAsync(cipherDetails);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException(
|
||||
@ -1536,25 +1401,12 @@ public class CipherServiceTests
|
||||
cipherDetails.UserId = Guid.NewGuid();
|
||||
cipherDetails.OrganizationId = null;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteAsync(cipherDetails, deletingUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().DeleteAsync(default);
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().DidNotReceiveWithAnyArgs().DeleteAttachmentsForCipherAsync(default);
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherDeleteAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipherDetails.Id)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = deletingUserId,
|
||||
});
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteAsync(cipherDetails, deletingUserId));
|
||||
@ -1583,16 +1435,13 @@ public class CipherServiceTests
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithLimitItemDeletionEnabled_WithManagePermission_DeletesCipher(
|
||||
public async Task DeleteAsync_WithManagePermission_DeletesCipher(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipherDetails.OrganizationId = Guid.NewGuid();
|
||||
cipherDetails.Edit = false;
|
||||
cipherDetails.Manage = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(user);
|
||||
@ -1615,16 +1464,13 @@ public class CipherServiceTests
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAsync_WithLimitItemDeletionEnabled_WithoutManagePermission_ThrowsBadRequestException(
|
||||
public async Task DeleteAsync_WithoutManagePermission_ThrowsBadRequestException(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipherDetails.OrganizationId = Guid.NewGuid();
|
||||
cipherDetails.Edit = true;
|
||||
cipherDetails.Manage = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(user);
|
||||
@ -1691,6 +1537,12 @@ public class CipherServiceTests
|
||||
cipher.Edit = true;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = deletingUserId,
|
||||
});
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(ciphers);
|
||||
@ -1740,71 +1592,14 @@ public class CipherServiceTests
|
||||
.PushSyncCiphersAsync(deletingUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyAsync_WithOrgCipherAndEditPermission_DeletesCiphers(
|
||||
Guid deletingUserId, List<CipherDetails> ciphers, Guid organizationId, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var cipherIds = ciphers.Select(c => c.Id).ToArray();
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
cipher.OrganizationId = organizationId;
|
||||
cipher.Edit = true;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(ciphers);
|
||||
|
||||
await sutProvider.Sut.DeleteManyAsync(cipherIds, deletingUserId, organizationId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.DeleteAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == cipherIds.Count() && ids.All(id => cipherIds.Contains(id))), deletingUserId);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogCipherEventsAsync(Arg.Any<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncCiphersAsync(deletingUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyAsync_WithOrgCipherLackingEditPermission_DoesNotDeleteCiphers(
|
||||
Guid deletingUserId, List<Cipher> ciphers, Guid organizationId, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var cipherIds = ciphers.Select(c => c.Id).ToArray();
|
||||
var cipherDetailsList = ciphers.Select(c => new CipherDetails
|
||||
{
|
||||
Id = c.Id,
|
||||
OrganizationId = organizationId,
|
||||
Edit = false
|
||||
}).ToList();
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(cipherDetailsList);
|
||||
|
||||
await sutProvider.Sut.DeleteManyAsync(cipherIds, deletingUserId, organizationId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.DeleteAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()), deletingUserId);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogCipherEventsAsync(Arg.Any<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncCiphersAsync(deletingUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyAsync_WithLimitItemDeletionEnabled_WithoutManagePermission_DoesNotDeleteCiphers(
|
||||
public async Task DeleteManyAsync_WithoutManagePermission_DoesNotDeleteCiphers(
|
||||
Guid deletingUserId, List<CipherDetails> ciphers, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
@ -1817,9 +1612,6 @@ public class CipherServiceTests
|
||||
cipher.Manage = false;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(ciphers);
|
||||
@ -1855,7 +1647,7 @@ public class CipherServiceTests
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task DeleteManyAsync_WithLimitItemDeletionEnabled_WithManagePermission_DeletesCiphers(
|
||||
public async Task DeleteManyAsync_WithManagePermission_DeletesCiphers(
|
||||
Guid deletingUserId, List<CipherDetails> ciphers, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
@ -1868,9 +1660,6 @@ public class CipherServiceTests
|
||||
cipher.Manage = true;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(ciphers);
|
||||
@ -1913,9 +1702,12 @@ public class CipherServiceTests
|
||||
cipherDetails.OrganizationId = null;
|
||||
cipherDetails.DeletedDate = null;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipherDetails.Id)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = deletingUserId,
|
||||
});
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipherDetails, deletingUserId);
|
||||
|
||||
@ -1926,26 +1718,7 @@ public class CipherServiceTests
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipherDetails, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithOrgCipherAndEditPermission_SoftDeletesCipher(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipherDetails.DeletedDate = null;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipherDetails.Id)
|
||||
.Returns(true);
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipherDetails, deletingUserId);
|
||||
|
||||
Assert.NotNull(cipherDetails.DeletedDate);
|
||||
Assert.Equal(cipherDetails.RevisionDate, cipherDetails.DeletedDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipherDetails);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipherDetails, EventType.Cipher_SoftDeleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipherDetails, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
@ -1955,9 +1728,12 @@ public class CipherServiceTests
|
||||
cipherDetails.UserId = Guid.NewGuid();
|
||||
cipherDetails.OrganizationId = null;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipherDetails.Id)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = deletingUserId,
|
||||
});
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SoftDeleteAsync(cipherDetails, deletingUserId));
|
||||
@ -1965,48 +1741,23 @@ public class CipherServiceTests
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipherDetails.Id)
|
||||
.Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SoftDeleteAsync(cipherDetails, deletingUserId));
|
||||
|
||||
Assert.Contains("do not have permissions", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithEditPermission_SoftDeletesCipherDetails(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipherDetails.DeletedDate = null;
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipherDetails, deletingUserId, true);
|
||||
|
||||
Assert.NotNull(cipherDetails.DeletedDate);
|
||||
Assert.Equal(cipherDetails.RevisionDate, cipherDetails.DeletedDate);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipherDetails);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipherDetails, EventType.Cipher_SoftDeleted);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipherDetails, null);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithAlreadySoftDeletedCipher_SkipsOperation(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetCanEditByIdAsync(deletingUserId, cipherDetails.Id)
|
||||
.Returns(true);
|
||||
// Set up as personal cipher owned by the deleting user
|
||||
cipherDetails.UserId = deletingUserId;
|
||||
cipherDetails.OrganizationId = null;
|
||||
cipherDetails.DeletedDate = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = deletingUserId,
|
||||
});
|
||||
|
||||
await sutProvider.Sut.SoftDeleteAsync(cipherDetails, deletingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpsertAsync(Arg.Any<Cipher>());
|
||||
@ -2032,7 +1783,7 @@ public class CipherServiceTests
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithLimitItemDeletionEnabled_WithManagePermission_SoftDeletesCipher(
|
||||
public async Task SoftDeleteAsync_WithManagePermission_SoftDeletesCipher(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipherDetails.OrganizationId = Guid.NewGuid();
|
||||
@ -2040,9 +1791,6 @@ public class CipherServiceTests
|
||||
cipherDetails.Edit = false;
|
||||
cipherDetails.Manage = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(user);
|
||||
@ -2066,7 +1814,7 @@ public class CipherServiceTests
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteAsync_WithLimitItemDeletionEnabled_WithoutManagePermission_ThrowsBadRequestException(
|
||||
public async Task SoftDeleteAsync_WithoutManagePermission_ThrowsBadRequestException(
|
||||
Guid deletingUserId, CipherDetails cipherDetails, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
cipherDetails.OrganizationId = Guid.NewGuid();
|
||||
@ -2074,9 +1822,6 @@ public class CipherServiceTests
|
||||
cipherDetails.Edit = true;
|
||||
cipherDetails.Manage = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(user);
|
||||
@ -2143,6 +1888,12 @@ public class CipherServiceTests
|
||||
cipher.DeletedDate = null;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(deletingUserId)
|
||||
.Returns(new User
|
||||
{
|
||||
Id = deletingUserId,
|
||||
});
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(ciphers);
|
||||
@ -2192,72 +1943,14 @@ public class CipherServiceTests
|
||||
.PushSyncCiphersAsync(deletingUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteManyAsync_WithOrgCipherAndEditPermission_SoftDeletesCiphers(
|
||||
Guid deletingUserId, List<CipherDetails> ciphers, Guid organizationId, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var cipherIds = ciphers.Select(c => c.Id).ToArray();
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
cipher.OrganizationId = organizationId;
|
||||
cipher.Edit = true;
|
||||
cipher.DeletedDate = null;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(ciphers);
|
||||
|
||||
await sutProvider.Sut.SoftDeleteManyAsync(cipherIds, deletingUserId, organizationId, false);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.SoftDeleteAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == cipherIds.Count() && ids.All(id => cipherIds.Contains(id))), deletingUserId);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogCipherEventsAsync(Arg.Any<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncCiphersAsync(deletingUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteManyAsync_WithOrgCipherLackingEditPermission_DoesNotDeleteCiphers(
|
||||
Guid deletingUserId, List<Cipher> ciphers, Guid organizationId, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var cipherIds = ciphers.Select(c => c.Id).ToArray();
|
||||
var cipherDetailsList = ciphers.Select(c => new CipherDetails
|
||||
{
|
||||
Id = c.Id,
|
||||
OrganizationId = organizationId,
|
||||
Edit = false
|
||||
}).ToList();
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(cipherDetailsList);
|
||||
|
||||
await sutProvider.Sut.SoftDeleteManyAsync(cipherIds, deletingUserId, organizationId, false);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.SoftDeleteAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()), deletingUserId);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogCipherEventsAsync(Arg.Any<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncCiphersAsync(deletingUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteManyAsync_WithLimitItemDeletionEnabled_WithoutManagePermission_DoesNotDeleteCiphers(
|
||||
public async Task SoftDeleteManyAsync_WithoutManagePermission_DoesNotDeleteCiphers(
|
||||
Guid deletingUserId, List<CipherDetails> ciphers, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
@ -2270,9 +1963,6 @@ public class CipherServiceTests
|
||||
cipher.Manage = false;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(ciphers);
|
||||
@ -2308,7 +1998,7 @@ public class CipherServiceTests
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
public async Task SoftDeleteManyAsync_WithLimitItemDeletionEnabled_WithManagePermission_SoftDeletesCiphers(
|
||||
public async Task SoftDeleteManyAsync_WithManagePermission_SoftDeletesCiphers(
|
||||
Guid deletingUserId, List<CipherDetails> ciphers, User user, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
@ -2322,9 +2012,6 @@ public class CipherServiceTests
|
||||
cipher.DeletedDate = null;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitItemDeletion)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(deletingUserId)
|
||||
.Returns(ciphers);
|
||||
|
@ -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;
|
||||
@ -239,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)
|
||||
{
|
||||
@ -278,7 +277,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,10 +7,10 @@ using Bit.Infrastructure.EFIntegration.Test.Helpers;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||
using Bit.Infrastructure.EntityFramework.Auth.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Dirt.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Platform;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Tools.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -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;
|
||||
|
@ -1,9 +1,9 @@
|
||||
using AutoFixture;
|
||||
using AutoFixture.Kernel;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Dirt.Reports.Entities;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Dirt.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Tools.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
||||
|
@ -1,17 +1,16 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Dirt.Reports.Entities;
|
||||
using Bit.Core.Dirt.Reports.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Infrastructure.Dapper.Dirt;
|
||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||
using Xunit;
|
||||
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using EfToolsRepo = Bit.Infrastructure.EntityFramework.Tools.Repositories;
|
||||
using SqlAdminConsoleRepo = Bit.Infrastructure.Dapper.Tools.Repositories;
|
||||
using SqlRepo = Bit.Infrastructure.Dapper.Repositories;
|
||||
|
||||
namespace Bit.Infrastructure.EFIntegration.Test.Tools.Repositories;
|
||||
namespace Bit.Infrastructure.EFIntegration.Test.Dirt.Repositories;
|
||||
|
||||
public class PasswordHealthReportApplicationRepositoryTests
|
||||
{
|
||||
@ -19,9 +18,9 @@ public class PasswordHealthReportApplicationRepositoryTests
|
||||
public async Task CreateAsync_Works_DataMatches(
|
||||
PasswordHealthReportApplication passwordHealthReportApplication,
|
||||
Organization organization,
|
||||
List<EfToolsRepo.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EntityFramework.Dirt.Repositories.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EfRepo.OrganizationRepository> efOrganizationRepos,
|
||||
SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrganizationRepo
|
||||
)
|
||||
{
|
||||
@ -53,7 +52,7 @@ public class PasswordHealthReportApplicationRepositoryTests
|
||||
|
||||
[CiSkippedTheory, EfPasswordHealthReportApplicationAutoData]
|
||||
public async Task RetrieveByOrganisation_Works(
|
||||
SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrganizationRepo)
|
||||
{
|
||||
var (firstOrg, firstRecord) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo);
|
||||
@ -68,9 +67,9 @@ public class PasswordHealthReportApplicationRepositoryTests
|
||||
|
||||
[CiSkippedTheory, EfPasswordHealthReportApplicationAutoData]
|
||||
public async Task ReplaceQuery_Works(
|
||||
List<EfToolsRepo.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EntityFramework.Dirt.Repositories.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EfRepo.OrganizationRepository> efOrganizationRepos,
|
||||
SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrganizationRepo)
|
||||
{
|
||||
var (org, pwdRecord) = await CreateSampleRecord(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo);
|
||||
@ -127,9 +126,9 @@ public class PasswordHealthReportApplicationRepositoryTests
|
||||
|
||||
[CiSkippedTheory, EfPasswordHealthReportApplicationAutoData]
|
||||
public async Task Upsert_Works(
|
||||
List<EfToolsRepo.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EntityFramework.Dirt.Repositories.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EfRepo.OrganizationRepository> efOrganizationRepos,
|
||||
SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrganizationRepo)
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
@ -204,9 +203,9 @@ public class PasswordHealthReportApplicationRepositoryTests
|
||||
|
||||
[CiSkippedTheory, EfPasswordHealthReportApplicationAutoData]
|
||||
public async Task Delete_Works(
|
||||
List<EfToolsRepo.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EntityFramework.Dirt.Repositories.PasswordHealthReportApplicationRepository> suts,
|
||||
List<EfRepo.OrganizationRepository> efOrganizationRepos,
|
||||
SqlAdminConsoleRepo.PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
PasswordHealthReportApplicationRepository sqlPasswordHealthReportApplicationRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrganizationRepo)
|
||||
{
|
||||
var fixture = new Fixture();
|
@ -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]);
|
||||
}
|
||||
}
|
@ -286,4 +286,141 @@ public class OrganizationRepositoryTests
|
||||
await organizationRepository.DeleteAsync(organization1);
|
||||
await organizationRepository.DeleteAsync(organization2);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithUsersAndSponsorships_ReturnsCorrectCounts(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
// Create users in different states
|
||||
var user1 = await userRepository.CreateTestUserAsync("test1");
|
||||
var user2 = await userRepository.CreateTestUserAsync("test2");
|
||||
var user3 = await userRepository.CreateTestUserAsync("test3");
|
||||
|
||||
// Create organization users in different states
|
||||
await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1); // Confirmed state
|
||||
await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization); // Invited state
|
||||
|
||||
// Create a revoked user manually since there's no helper for it
|
||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user3.Id,
|
||||
Status = OrganizationUserStatusType.Revoked,
|
||||
});
|
||||
|
||||
// Create sponsorships in different states
|
||||
await organizationSponsorshipRepository.CreateAsync(new OrganizationSponsorship
|
||||
{
|
||||
SponsoringOrganizationId = organization.Id,
|
||||
IsAdminInitiated = true,
|
||||
ToDelete = false,
|
||||
ValidUntil = null,
|
||||
});
|
||||
|
||||
await organizationSponsorshipRepository.CreateAsync(new OrganizationSponsorship
|
||||
{
|
||||
SponsoringOrganizationId = organization.Id,
|
||||
IsAdminInitiated = true,
|
||||
ToDelete = true,
|
||||
ValidUntil = DateTime.UtcNow.AddDays(1),
|
||||
});
|
||||
|
||||
await organizationSponsorshipRepository.CreateAsync(new OrganizationSponsorship
|
||||
{
|
||||
SponsoringOrganizationId = organization.Id,
|
||||
IsAdminInitiated = true,
|
||||
ToDelete = true,
|
||||
ValidUntil = DateTime.UtcNow.AddDays(-1), // Expired
|
||||
});
|
||||
|
||||
await organizationSponsorshipRepository.CreateAsync(new OrganizationSponsorship
|
||||
{
|
||||
SponsoringOrganizationId = organization.Id,
|
||||
IsAdminInitiated = false, // Not admin initiated
|
||||
ToDelete = false,
|
||||
ValidUntil = null,
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Users); // Confirmed + Invited users
|
||||
Assert.Equal(2, result.Sponsored); // Two valid sponsorships
|
||||
Assert.Equal(4, result.Total); // Total occupied seats
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithNoUsersOrSponsorships_ReturnsZero(
|
||||
IOrganizationRepository organizationRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
// Act
|
||||
var result = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result.Users);
|
||||
Assert.Equal(0, result.Sponsored);
|
||||
Assert.Equal(0, result.Total);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithOnlyRevokedUsers_ReturnsZero(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var user = await userRepository.CreateTestUserAsync("test1");
|
||||
|
||||
await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Revoked,
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result.Users);
|
||||
Assert.Equal(0, result.Sponsored);
|
||||
Assert.Equal(0, result.Total);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithOnlyExpiredSponsorships_ReturnsZero(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
await organizationSponsorshipRepository.CreateAsync(new OrganizationSponsorship
|
||||
{
|
||||
SponsoringOrganizationId = organization.Id,
|
||||
IsAdminInitiated = true,
|
||||
ToDelete = true,
|
||||
ValidUntil = DateTime.UtcNow.AddDays(-1), // Expired
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result.Users);
|
||||
Assert.Equal(0, result.Sponsored);
|
||||
Assert.Equal(0, result.Total);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
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