1
0
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:
Ike
2025-06-12 18:00:06 -04:00
committed by GitHub
594 changed files with 38113 additions and 5486 deletions

View File

@ -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();

View File

@ -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

View File

@ -0,0 +1,7 @@
using Bit.IntegrationTestCommon;
#nullable enable
namespace Bit.Api.IntegrationTest.Factories;
public class SqlServerApiApplicationFactory() : ApiApplicationFactory(new SqlServerTestDatabase());

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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));

View File

@ -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));

View File

@ -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

View File

@ -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,

View File

@ -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" />

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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));
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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));
}

View File

@ -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);

View File

@ -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(

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -288,7 +288,7 @@ public class SavePolicyCommandTests
{
return new SutProvider<SavePolicyCommand>()
.WithFakeTimeProvider()
.SetDependency(typeof(IEnumerable<IPolicyValidator>), policyValidators ?? [])
.SetDependency(policyValidators ?? [])
.Create();
}

View File

@ -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" })));
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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" })));
}
}
}

View File

@ -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);
}
}
}

View 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());
}
}

View File

@ -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));

View File

@ -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" })));
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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))
);
}
}

View File

@ -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);
}

View File

@ -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()
);
}
}

View File

@ -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);
}
}

View File

@ -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]

View File

@ -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]

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.";

View File

@ -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>()

View File

@ -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();
}

View File

@ -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]

View File

@ -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]

View 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
};
}
}

View File

@ -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);
}

View File

@ -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}");
}
}
}
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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)

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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);
}
}
}

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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)

View File

@ -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()}"
});
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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]);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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
{

View File

@ -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();
}
}

View 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
}
}

View File

@ -11,6 +11,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Identity\Identity.csproj" />
<ProjectReference Include="..\..\util\Migrator\Migrator.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View 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();
}
}
}

View 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();
}
}