1
0
mirror of https://github.com/bitwarden/server synced 2026-01-21 03:43:53 +00:00

Merge branch 'main' into tools/pm-21918/send-authentication-commands

This commit is contained in:
✨ Audrey ✨
2025-09-17 13:41:54 -04:00
committed by GitHub
579 changed files with 52348 additions and 4739 deletions

View File

@@ -0,0 +1,403 @@
using System.Collections.Concurrent;
using Bit.Core.AdminConsole.AbilitiesCache;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.AbilitiesCache;
[SutProviderCustomize]
public class VNextInMemoryApplicationCacheServiceTests
{
[Theory, BitAutoData]
public async Task GetOrganizationAbilitiesAsync_FirstCall_LoadsFromRepository(
ICollection<OrganizationAbility> organizationAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
// Act
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Assert
Assert.IsType<ConcurrentDictionary<Guid, OrganizationAbility>>(result);
Assert.Equal(organizationAbilities.Count, result.Count);
foreach (var ability in organizationAbilities)
{
Assert.True(result.TryGetValue(ability.Id, out var actualAbility));
Assert.Equal(ability, actualAbility);
}
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilitiesAsync_SecondCall_UsesCachedValue(
List<OrganizationAbility> organizationAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
// Act
var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Assert
Assert.Same(firstCall, secondCall);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilityAsync_ExistingId_ReturnsAbility(
List<OrganizationAbility> organizationAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
var targetAbility = organizationAbilities.First();
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
// Act
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(targetAbility.Id);
// Assert
Assert.Equal(targetAbility, result);
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilityAsync_NonExistingId_ReturnsNull(
List<OrganizationAbility> organizationAbilities,
Guid nonExistingId,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
// Act
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(nonExistingId);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public async Task GetProviderAbilitiesAsync_FirstCall_LoadsFromRepository(
List<ProviderAbility> providerAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(providerAbilities);
// Act
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
// Assert
Assert.IsType<ConcurrentDictionary<Guid, ProviderAbility>>(result);
Assert.Equal(providerAbilities.Count, result.Count);
foreach (var ability in providerAbilities)
{
Assert.True(result.TryGetValue(ability.Id, out var actualAbility));
Assert.Equal(ability, actualAbility);
}
await sutProvider.GetDependency<IProviderRepository>().Received(1).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetProviderAbilitiesAsync_SecondCall_UsesCachedValue(
List<ProviderAbility> providerAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(providerAbilities);
// Act
var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
// Assert
Assert.Same(firstCall, secondCall);
await sutProvider.GetDependency<IProviderRepository>().Received(1).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task UpsertOrganizationAbilityAsync_NewOrganization_AddsToCache(
Organization organization,
List<OrganizationAbility> existingAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(existingAbilities);
await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Act
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
// Assert
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
Assert.True(result.ContainsKey(organization.Id));
Assert.Equal(organization.Id, result[organization.Id].Id);
}
[Theory, BitAutoData]
public async Task UpsertOrganizationAbilityAsync_ExistingOrganization_UpdatesCache(
Organization organization,
List<OrganizationAbility> existingAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
existingAbilities.Add(new OrganizationAbility { Id = organization.Id });
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(existingAbilities);
await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Act
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
// Assert
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
Assert.True(result.ContainsKey(organization.Id));
Assert.Equal(organization.Id, result[organization.Id].Id);
}
[Theory, BitAutoData]
public async Task UpsertProviderAbilityAsync_NewProvider_AddsToCache(
Provider provider,
List<ProviderAbility> existingAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(existingAbilities);
await sutProvider.Sut.GetProviderAbilitiesAsync();
// Act
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
// Assert
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
Assert.True(result.ContainsKey(provider.Id));
Assert.Equal(provider.Id, result[provider.Id].Id);
}
[Theory, BitAutoData]
public async Task DeleteOrganizationAbilityAsync_ExistingId_RemovesFromCache(
List<OrganizationAbility> organizationAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
var targetAbility = organizationAbilities.First();
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Act
await sutProvider.Sut.DeleteOrganizationAbilityAsync(targetAbility.Id);
// Assert
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
Assert.False(result.ContainsKey(targetAbility.Id));
}
[Theory, BitAutoData]
public async Task DeleteProviderAbilityAsync_ExistingId_RemovesFromCache(
List<ProviderAbility> providerAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
var targetAbility = providerAbilities.First();
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(providerAbilities);
await sutProvider.Sut.GetProviderAbilitiesAsync();
// Act
await sutProvider.Sut.DeleteProviderAbilityAsync(targetAbility.Id);
// Assert
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
Assert.False(result.ContainsKey(targetAbility.Id));
}
[Theory, BitAutoData]
public async Task ConcurrentAccess_GetOrganizationAbilities_ThreadSafe(
List<OrganizationAbility> organizationAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
var results = new ConcurrentBag<IDictionary<Guid, OrganizationAbility>>();
const int iterationCount = 100;
// Act
await Parallel.ForEachAsync(
Enumerable.Range(0, iterationCount),
async (_, _) =>
{
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
results.Add(result);
});
// Assert
var firstCall = results.First();
Assert.Equal(iterationCount, results.Count);
Assert.All(results, result => Assert.Same(firstCall, result));
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilitiesAsync_AfterRefreshInterval_RefreshesFromRepository(
List<OrganizationAbility> organizationAbilities,
List<OrganizationAbility> updatedAbilities)
{
// Arrange
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
.WithFakeTimeProvider()
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities, updatedAbilities);
var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
const int pastIntervalInMinutes = 11;
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes);
// Act
var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Assert
Assert.NotSame(firstCall, secondCall);
Assert.Equal(updatedAbilities.Count, secondCall.Count);
await sutProvider.GetDependency<IOrganizationRepository>().Received(2).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetProviderAbilitiesAsync_AfterRefreshInterval_RefreshesFromRepository(
List<ProviderAbility> providerAbilities,
List<ProviderAbility> updatedAbilities)
{
// Arrange
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
.WithFakeTimeProvider()
.Create();
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(providerAbilities, updatedAbilities);
var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
const int pastIntervalMinutes = 15;
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalMinutes);
// Act
var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
// Assert
Assert.NotSame(firstCall, secondCall);
Assert.Equal(updatedAbilities.Count, secondCall.Count);
await sutProvider.GetDependency<IProviderRepository>().Received(2).GetManyAbilitiesAsync();
}
public static IEnumerable<object[]> WhenCacheIsWithinIntervalTestCases =>
[
[5, 1],
[10, 1],
];
[Theory]
[BitMemberAutoData(nameof(WhenCacheIsWithinIntervalTestCases))]
public async Task GetOrganizationAbilitiesAsync_WhenCacheIsWithinInterval(
int pastIntervalInMinutes,
int expectCacheHit,
List<OrganizationAbility> organizationAbilities)
{
// Arrange
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
.WithFakeTimeProvider()
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes);
// Act
var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Assert
Assert.Same(firstCall, secondCall);
Assert.Equal(organizationAbilities.Count, secondCall.Count);
await sutProvider.GetDependency<IOrganizationRepository>().Received(expectCacheHit).GetManyAbilitiesAsync();
}
[Theory]
[BitMemberAutoData(nameof(WhenCacheIsWithinIntervalTestCases))]
public async Task GetProviderAbilitiesAsync_WhenCacheIsWithinInterval(
int pastIntervalInMinutes,
int expectCacheHit,
List<ProviderAbility> providerAbilities)
{
// Arrange
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
.WithFakeTimeProvider()
.Create();
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(providerAbilities);
var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes);
// Act
var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
// Assert
Assert.Same(firstCall, secondCall);
Assert.Equal(providerAbilities.Count, secondCall.Count);
await sutProvider.GetDependency<IProviderRepository>().Received(expectCacheHit).GetManyAbilitiesAsync();
}
private static void SimulateTimeLapseAfterFirstCall(SutProvider<VNextInMemoryApplicationCacheService> sutProvider, int pastIntervalInMinutes) =>
sutProvider
.GetDependency<FakeTimeProvider>()
.Advance(TimeSpan.FromMinutes(pastIntervalInMinutes));
}

View File

@@ -0,0 +1,35 @@
using System.Reflection;
using AutoFixture;
using AutoFixture.Xunit2;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.Enums;
namespace Bit.Core.Test.AdminConsole.AutoFixture;
internal class OrganizationPolicyDetailsCustomization(
PolicyType policyType,
OrganizationUserType userType,
bool isProvider,
OrganizationUserStatusType userStatus) : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<OrganizationPolicyDetails>(composer => composer
.With(o => o.PolicyType, policyType)
.With(o => o.OrganizationUserType, userType)
.With(o => o.IsProvider, isProvider)
.With(o => o.OrganizationUserStatus, userStatus)
.Without(o => o.PolicyData)); // avoid autogenerating invalid json data
}
}
public class OrganizationPolicyDetailsAttribute(
PolicyType policyType,
OrganizationUserType userType = OrganizationUserType.User,
bool isProvider = false,
OrganizationUserStatusType userStatus = OrganizationUserStatusType.Confirmed) : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
=> new OrganizationPolicyDetailsCustomization(policyType, userType, isProvider, userStatus);
}

View File

@@ -33,3 +33,5 @@ public class PolicyDetailsAttribute(
public override ICustomization GetCustomization(ParameterInfo parameter)
=> new PolicyDetailsCustomization(policyType, userType, isProvider, userStatus);
}

View File

@@ -18,7 +18,7 @@ internal class PolicyUpdateCustomization(PolicyType type, bool enabled) : ICusto
}
}
public class PolicyUpdateAttribute(PolicyType type, bool enabled = true) : CustomizeAttribute
public class PolicyUpdateAttribute(PolicyType type = PolicyType.FreeFamiliesSponsorshipPolicy, bool enabled = true) : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
{

View File

@@ -0,0 +1,102 @@
#nullable enable
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Entities;
using Bit.Core.Models.Data;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations;
public class IntegrationTemplateContextTests
{
[Theory, BitAutoData]
public void EventMessage_ReturnsSerializedJsonOfEvent(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage: eventMessage);
var expected = JsonSerializer.Serialize(eventMessage);
Assert.Equal(expected, sut.EventMessage);
}
[Theory, BitAutoData]
public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, User user)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = user };
Assert.Equal(user.Name, sut.UserName);
}
[Theory, BitAutoData]
public void UserName_WhenUserIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = null };
Assert.Null(sut.UserName);
}
[Theory, BitAutoData]
public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, User user)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = user };
Assert.Equal(user.Email, sut.UserEmail);
}
[Theory, BitAutoData]
public void UserEmail_WhenUserIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = null };
Assert.Null(sut.UserEmail);
}
[Theory, BitAutoData]
public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, User actingUser)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
Assert.Equal(actingUser.Name, sut.ActingUserName);
}
[Theory, BitAutoData]
public void ActingUserName_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null };
Assert.Null(sut.ActingUserName);
}
[Theory, BitAutoData]
public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, User actingUser)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
Assert.Equal(actingUser.Email, sut.ActingUserEmail);
}
[Theory, BitAutoData]
public void ActingUserEmail_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null };
Assert.Null(sut.ActingUserEmail);
}
[Theory, BitAutoData]
public void OrganizationName_WhenOrganizationIsSet_ReturnsDisplayName(EventMessage eventMessage, Organization organization)
{
var sut = new IntegrationTemplateContext(eventMessage) { Organization = organization };
Assert.Equal(organization.DisplayName(), sut.OrganizationName);
}
[Theory, BitAutoData]
public void OrganizationName_WhenOrganizationIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { Organization = null };
Assert.Null(sut.OrganizationName);
}
}

View File

@@ -98,8 +98,6 @@ public class ImportOrganizationUsersAndGroupsCommandTests
SetupOrganizationConfigForImport(sutProvider, org, existingUsers, []);
// Existing user does not have a master password
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.DirectoryConnectorPreventUserRemoval)
.Returns(true);
existingUsers.First().HasMasterPassword = false;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);

View File

@@ -479,7 +479,7 @@ public class ConfirmOrganizationUserCommandTests
await sutProvider.GetDependency<ICollectionRepository>()
.Received(1)
.CreateDefaultCollectionsAsync(
.UpsertDefaultCollectionsAsync(
organization.Id,
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.Id)),
collectionName);
@@ -505,7 +505,7 @@ public class ConfirmOrganizationUserCommandTests
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive()
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
}
[Theory, BitAutoData]
@@ -531,6 +531,6 @@ public class ConfirmOrganizationUserCommandTests
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive()
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
}
}

View File

@@ -0,0 +1,467 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
[SutProviderCustomize]
public class DeleteClaimedOrganizationUserAccountCommandvNextTests
{
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithValidSingleUser_CallsDeleteManyUsersAsync(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
organizationUser.OrganizationId = organizationId;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var validationResult = CreateSuccessfulValidationResult(request);
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { organizationUser },
[user],
organizationId,
new Dictionary<Guid, bool> { { organizationUser.Id, true } });
SetupValidatorMock(sutProvider, [validationResult]);
var result = await sutProvider.Sut.DeleteUserAsync(organizationId, organizationUser.Id, deletingUserId);
Assert.Equal(organizationUser.Id, result.Id);
Assert.True(result.Result.IsSuccess);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)));
await AssertSuccessfulUserOperations(sutProvider, [user], [organizationUser]);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithEmptyUserIds_ReturnsEmptyResults(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
Guid organizationId,
Guid deletingUserId)
{
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [], deletingUserId);
Assert.Empty(results);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
User user1,
User user2,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser1,
[OrganizationUser] OrganizationUser orgUser2)
{
// Arrange
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
orgUser1.UserId = user1.Id;
orgUser2.UserId = user2.Id;
var request1 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser1.Id,
OrganizationUser = orgUser1,
User = user1,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var request2 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser2.Id,
OrganizationUser = orgUser2,
User = user2,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var validationResults = new[]
{
CreateSuccessfulValidationResult(request1),
CreateSuccessfulValidationResult(request2)
};
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { orgUser1, orgUser2 },
[user1, user2],
organizationId,
new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, true } });
SetupValidatorMock(sutProvider, validationResults);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUser1.Id, orgUser2.Id], deletingUserId);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
Assert.All(resultsList, result => Assert.True(result.Result.IsSuccess));
await AssertSuccessfulUserOperations(sutProvider, [user1, user2], [orgUser1, orgUser2]);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithValidationErrors_ReturnsErrorResults(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
Guid organizationId,
Guid orgUserId1,
Guid orgUserId2,
Guid deletingUserId)
{
// Arrange
var request1 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUserId1,
DeletingUserId = deletingUserId
};
var request2 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUserId2,
DeletingUserId = deletingUserId
};
var validationResults = new[]
{
CreateFailedValidationResult(request1, new UserNotClaimedError()),
CreateFailedValidationResult(request2, new InvalidUserStatusError())
};
SetupRepositoryMocks(sutProvider, [], [], organizationId, new Dictionary<Guid, bool>());
SetupValidatorMock(sutProvider, validationResults);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserId1, orgUserId2], deletingUserId);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
Assert.Equal(orgUserId1, resultsList[0].Id);
Assert.True(resultsList[0].Result.IsError);
Assert.IsType<UserNotClaimedError>(resultsList[0].Result.AsError);
Assert.Equal(orgUserId2, resultsList[1].Id);
Assert.True(resultsList[1].Result.IsError);
Assert.IsType<InvalidUserStatusError>(resultsList[1].Result.AsError);
await AssertNoUserOperations(sutProvider);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithMixedValidationResults_HandlesPartialSuccessCorrectly(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
User validUser,
Guid organizationId,
Guid validOrgUserId,
Guid invalidOrgUserId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser validOrgUser)
{
validOrgUser.Id = validOrgUserId;
validOrgUser.UserId = validUser.Id;
validOrgUser.OrganizationId = organizationId;
var validRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = validOrgUserId,
OrganizationUser = validOrgUser,
User = validUser,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var invalidRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = invalidOrgUserId,
DeletingUserId = deletingUserId
};
var validationResults = new[]
{
CreateSuccessfulValidationResult(validRequest),
CreateFailedValidationResult(invalidRequest, new UserNotFoundError())
};
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { validOrgUser },
[validUser],
organizationId,
new Dictionary<Guid, bool> { { validOrgUserId, true } });
SetupValidatorMock(sutProvider, validationResults);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [validOrgUserId, invalidOrgUserId], deletingUserId);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
var validResult = resultsList.First(r => r.Id == validOrgUserId);
var invalidResult = resultsList.First(r => r.Id == invalidOrgUserId);
Assert.True(validResult.Result.IsSuccess);
Assert.True(invalidResult.Result.IsError);
Assert.IsType<UserNotFoundError>(invalidResult.Result.AsError);
await AssertSuccessfulUserOperations(sutProvider, [validUser], [validOrgUser]);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_CancelPremiumsAsync_HandlesGatewayExceptionAndLogsWarning(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser)
{
orgUser.UserId = user.Id;
orgUser.OrganizationId = organizationId;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser.Id,
OrganizationUser = orgUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var validationResult = CreateSuccessfulValidationResult(request);
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { orgUser },
[user],
organizationId,
new Dictionary<Guid, bool> { { orgUser.Id, true } });
SetupValidatorMock(sutProvider, [validationResult]);
var gatewayException = new GatewayException("Payment gateway error");
sutProvider.GetDependency<IUserService>()
.CancelPremiumAsync(user)
.ThrowsAsync(gatewayException);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUser.Id], deletingUserId);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList.First().Result.IsSuccess);
await sutProvider.GetDependency<IUserService>().Received(1).CancelPremiumAsync(user);
await AssertSuccessfulUserOperations(sutProvider, [user], [orgUser]);
sutProvider.GetDependency<ILogger<DeleteClaimedOrganizationUserAccountCommandvNext>>()
.Received(1)
.Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => o.ToString()!.Contains($"Failed to cancel premium subscription for {user.Id}")),
gatewayException,
Arg.Any<Func<object, Exception?, string>>());
}
[Theory]
[BitAutoData]
public async Task CreateInternalRequests_CreatesCorrectRequestsForAllUsers(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
User user1,
User user2,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser1,
[OrganizationUser] OrganizationUser orgUser2)
{
orgUser1.UserId = user1.Id;
orgUser2.UserId = user2.Id;
var orgUserIds = new[] { orgUser1.Id, orgUser2.Id };
var orgUsers = new List<OrganizationUser> { orgUser1, orgUser2 };
var users = new[] { user1, user2 };
var claimedStatuses = new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, false } };
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(orgUsers);
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id)))
.Returns(users);
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(claimedStatuses);
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(callInfo =>
{
var requests = callInfo.Arg<IEnumerable<DeleteUserValidationRequest>>();
return requests.Select(r => CreateFailedValidationResult(r, new UserNotFoundError()));
});
// Act
await sutProvider.Sut.DeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId);
// Assert
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
.Received(1)
.ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>
requests.Count() == 2 &&
requests.Any(r => r.OrganizationUserId == orgUser1.Id &&
r.OrganizationId == organizationId &&
r.OrganizationUser == orgUser1 &&
r.User == user1 &&
r.DeletingUserId == deletingUserId &&
r.IsClaimed == true) &&
requests.Any(r => r.OrganizationUserId == orgUser2.Id &&
r.OrganizationId == organizationId &&
r.OrganizationUser == orgUser2 &&
r.User == user2 &&
r.DeletingUserId == deletingUserId &&
r.IsClaimed == false)));
}
[Theory]
[BitAutoData]
public async Task GetUsersAsync_WithNullUserIds_ReturnsEmptyCollection(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUserWithoutUserId)
{
orgUserWithoutUserId.UserId = null; // Intentionally setting to null for test case
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUserWithoutUserId });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()))
.Returns([]);
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(callInfo =>
{
var requests = callInfo.Arg<IEnumerable<DeleteUserValidationRequest>>();
return requests.Select(r => CreateFailedValidationResult(r, new UserNotFoundError()));
});
// Act
await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserWithoutUserId.Id], deletingUserId);
// Assert
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
.Received(1)
.ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>
requests.Count() == 1 &&
requests.Single().User == null));
await sutProvider.GetDependency<IUserRepository>().Received(1)
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()));
}
private static ValidationResult<DeleteUserValidationRequest> CreateSuccessfulValidationResult(
DeleteUserValidationRequest request) =>
ValidationResultHelpers.Valid(request);
private static ValidationResult<DeleteUserValidationRequest> CreateFailedValidationResult(
DeleteUserValidationRequest request,
Error error) =>
ValidationResultHelpers.Invalid(request, error);
private static void SetupRepositoryMocks(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
ICollection<OrganizationUser> orgUsers,
IEnumerable<User> users,
Guid organizationId,
Dictionary<Guid, bool> claimedStatuses)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(orgUsers);
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(users);
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(claimedStatuses);
}
private static void SetupValidatorMock(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
IEnumerable<ValidationResult<DeleteUserValidationRequest>> validationResults)
{
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(validationResults);
}
private static async Task AssertSuccessfulUserOperations(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
IEnumerable<User> expectedUsers,
IEnumerable<OrganizationUser> expectedOrgUsers)
{
var userList = expectedUsers.ToList();
var orgUserList = expectedOrgUsers.ToList();
await sutProvider.GetDependency<IUserRepository>().Received(1)
.DeleteManyAsync(Arg.Is<IEnumerable<User>>(users =>
userList.All(expectedUser => users.Any(u => u.Id == expectedUser.Id))));
foreach (var user in userList)
{
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushLogOutAsync(user.Id);
}
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
orgUserList.All(expectedOrgUser =>
events.Any(e => e.Item1.Id == expectedOrgUser.Id && e.Item2 == EventType.OrganizationUser_Deleted))));
}
private static async Task AssertNoUserOperations(SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider)
{
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().DeleteManyAsync(default);
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushLogOutAsync(default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventsAsync(default(IEnumerable<(OrganizationUser, EventType, DateTime?)>));
}
}

View File

@@ -0,0 +1,503 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
[SutProviderCustomize]
public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
{
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithValidSingleRequest_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
organizationUser.OrganizationId = organizationId;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsValid);
Assert.Equal(request, resultsList[0].Request);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithMultipleValidRequests_ReturnsAllValidResults(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user1,
User user2,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2)
{
orgUser1.UserId = user1.Id;
orgUser1.OrganizationId = organizationId;
orgUser2.UserId = user2.Id;
orgUser2.OrganizationId = organizationId;
var request1 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser1.Id,
OrganizationUser = orgUser1,
User = user1,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var request2 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser2.Id,
OrganizationUser = orgUser2,
User = user2,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user1.Id);
SetupMocks(sutProvider, organizationId, user2.Id);
var results = await sutProvider.Sut.ValidateAsync([request1, request2]);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
Assert.All(resultsList, result => Assert.True(result.IsValid));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithNullUser_ReturnsUserNotFoundError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = null,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<UserNotFoundError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId)
{
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = Guid.NewGuid(),
OrganizationUser = null,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<UserNotFoundError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithInvitedUser_ReturnsInvalidUserStatusError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<InvalidUserStatusError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WhenDeletingYourself_ReturnsCannotDeleteYourselfError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = user.Id,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<CannotDeleteYourselfError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithUnclaimedUser_ReturnsUserNotClaimedError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = false
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<UserNotClaimedError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsNotOwner_ReturnsCannotDeleteOwnersError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Admin);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<CannotDeleteOwnersError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsOwner_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsValid);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithSoleOwnerOfOrganization_ReturnsSoleOwnerError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOnlyOwnerAsync(user.Id)
.Returns(1);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<SoleOwnerError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithSoleProviderOwner_ReturnsSoleProviderError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
sutProvider.GetDependency<IProviderUserRepository>()
.GetCountByOnlyOwnerAsync(user.Id)
.Returns(1);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<SoleProviderError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_CustomUserDeletingAdmin_ReturnsCannotDeleteAdminsError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Custom);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<CannotDeleteAdminsError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_AdminDeletingAdmin_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Admin);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsValid);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithMixedValidAndInvalidRequests_ReturnsCorrespondingResults(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
User validUser,
User invalidUser,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser validOrgUser,
[OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser invalidOrgUser)
{
validOrgUser.UserId = validUser.Id;
invalidOrgUser.UserId = invalidUser.Id;
var validRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = validOrgUser.Id,
OrganizationUser = validOrgUser,
User = validUser,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var invalidRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = invalidOrgUser.Id,
OrganizationUser = invalidOrgUser,
User = invalidUser,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, validUser.Id);
var results = await sutProvider.Sut.ValidateAsync([validRequest, invalidRequest]);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
var validResult = resultsList.First(r => r.Request == validRequest);
var invalidResult = resultsList.First(r => r.Request == invalidRequest);
Assert.True(validResult.IsValid);
Assert.True(invalidResult.IsError);
Assert.IsType<InvalidUserStatusError>(invalidResult.AsError);
}
private static void SetupMocks(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
Guid organizationId,
Guid userId,
OrganizationUserType currentUserType = OrganizationUserType.Owner)
{
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(currentUserType == OrganizationUserType.Owner);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationAdmin(organizationId)
.Returns(currentUserType is OrganizationUserType.Owner or OrganizationUserType.Admin);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationCustom(organizationId)
.Returns(currentUserType is OrganizationUserType.Custom);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOnlyOwnerAsync(userId)
.Returns(0);
sutProvider.GetDependency<IProviderUserRepository>()
.GetCountByOnlyOwnerAsync(userId)
.Returns(0);
}
}

View File

@@ -1,6 +1,7 @@
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.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -30,24 +31,85 @@ public class OrganizationDataOwnershipPolicyRequirementFactoryTests
}
[Theory, BitAutoData]
public void RequiresDefaultCollection_WithNoPolicies_ReturnsFalse(
Guid organizationId,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
public void PolicyType_ReturnsOrganizationDataOwnership(SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create([]);
Assert.False(actual.RequiresDefaultCollection(organizationId));
Assert.Equal(PolicyType.OrganizationDataOwnership, sutProvider.Sut.PolicyType);
}
[Theory, BitAutoData]
public void RequiresDefaultCollection_WithOrganizationDataOwnershipPolicies_ReturnsCorrectResult(
[PolicyDetails(PolicyType.OrganizationDataOwnership)] PolicyDetails[] policies,
Guid nonPolicyOrganizationId,
public void GetDefaultCollectionRequestOnPolicyEnable_WithConfirmedUser_ReturnsTrue(
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
// Arrange
var requirement = sutProvider.Sut.Create(policies);
var expectedOrganizationUserId = policies[0].OrganizationUserId;
var organizationId = policies[0].OrganizationId;
// Act
var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId);
// Assert
Assert.Equal(expectedOrganizationUserId, result.OrganizationUserId);
Assert.True(result.ShouldCreateDefaultCollection);
}
[Theory, BitAutoData]
public void GetDefaultCollectionRequestOnPolicyEnable_WithAcceptedUser_ReturnsFalse(
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Accepted)] PolicyDetails[] policies,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create(policies);
// Arrange
var requirement = sutProvider.Sut.Create(policies);
var organizationId = policies[0].OrganizationId;
Assert.True(actual.RequiresDefaultCollection(policies[0].OrganizationId));
Assert.False(actual.RequiresDefaultCollection(nonPolicyOrganizationId));
// Act
var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId);
// Assert
Assert.Equal(Guid.Empty, result.OrganizationUserId);
Assert.False(result.ShouldCreateDefaultCollection);
}
[Theory, BitAutoData]
public void GetDefaultCollectionRequestOnPolicyEnable_WithNoPolicies_ReturnsFalse(
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
// Arrange
var requirement = sutProvider.Sut.Create([]);
var organizationId = Guid.NewGuid();
// Act
var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId);
// Assert
Assert.Equal(Guid.Empty, result.OrganizationUserId);
Assert.False(result.ShouldCreateDefaultCollection);
}
[Theory, BitAutoData]
public void GetDefaultCollectionRequestOnPolicyEnable_WithMixedStatuses(
[PolicyDetails(PolicyType.OrganizationDataOwnership)] PolicyDetails[] policies,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
// Arrange
var requirement = sutProvider.Sut.Create(policies);
var confirmedPolicy = policies[0];
var acceptedPolicy = policies[1];
confirmedPolicy.OrganizationUserStatus = OrganizationUserStatusType.Confirmed;
acceptedPolicy.OrganizationUserStatus = OrganizationUserStatusType.Accepted;
// Act
var confirmedResult = requirement.GetDefaultCollectionRequestOnPolicyEnable(confirmedPolicy.OrganizationId);
var acceptedResult = requirement.GetDefaultCollectionRequestOnPolicyEnable(acceptedPolicy.OrganizationId);
// Assert
Assert.Equal(Guid.Empty, acceptedResult.OrganizationUserId);
Assert.False(acceptedResult.ShouldCreateDefaultCollection);
Assert.Equal(confirmedPolicy.OrganizationUserId, confirmedResult.OrganizationUserId);
Assert.True(confirmedResult.ShouldCreateDefaultCollection);
}
}

View File

@@ -0,0 +1,277 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
[SutProviderCustomize]
public class OrganizationDataOwnershipPolicyValidatorTests
{
private const string _defaultUserCollectionName = "Default";
[Theory, BitAutoData]
public async Task ExecuteSideEffectsAsync_FeatureFlagDisabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(false);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive()
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task ExecuteSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive()
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task ExecuteSideEffectsAsync_PolicyBeingDisabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive()
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task ExecuteSideEffectsAsync_WhenNoUsersExist_DoNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
OrganizationDataOwnershipPolicyRequirementFactory factory)
{
// Arrange
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
var policyRepository = ArrangePolicyRepository([]);
var collectionRepository = Substitute.For<ICollectionRepository>();
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await collectionRepository
.DidNotReceive()
.UpsertDefaultCollectionsAsync(
Arg.Any<Guid>(),
Arg.Any<IEnumerable<Guid>>(),
Arg.Any<string>());
await policyRepository
.Received(1)
.GetPolicyDetailsByOrganizationIdAsync(
policyUpdate.OrganizationId,
PolicyType.OrganizationDataOwnership);
}
public static IEnumerable<object?[]> ShouldUpsertDefaultCollectionsTestCases()
{
yield return WithExistingPolicy();
yield return WithNoExistingPolicy();
yield break;
object?[] WithExistingPolicy()
{
var organizationId = Guid.NewGuid();
var postUpdatedPolicy = new Policy
{
OrganizationId = organizationId,
Type = PolicyType.OrganizationDataOwnership,
Enabled = true
};
var previousPolicyState = new Policy
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
Type = PolicyType.OrganizationDataOwnership,
Enabled = false
};
return new object?[]
{
postUpdatedPolicy,
previousPolicyState
};
}
object?[] WithNoExistingPolicy()
{
var postUpdatedPolicy = new Policy
{
OrganizationId = new Guid(),
Type = PolicyType.OrganizationDataOwnership,
Enabled = true
};
const Policy previousPolicyState = null;
return new object?[]
{
postUpdatedPolicy,
previousPolicyState
};
}
}
[Theory]
[BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]
public async Task ExecuteSideEffectsAsync_WithRequirements_ShouldUpsertDefaultCollections(
Policy postUpdatedPolicy,
Policy? previousPolicyState,
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
[OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable<OrganizationPolicyDetails> orgPolicyDetails,
OrganizationDataOwnershipPolicyRequirementFactory factory)
{
// Arrange
var orgPolicyDetailsList = orgPolicyDetails.ToList();
foreach (var policyDetail in orgPolicyDetailsList)
{
policyDetail.OrganizationId = policyUpdate.OrganizationId;
}
var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);
var collectionRepository = Substitute.For<ICollectionRepository>();
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await collectionRepository
.Received(1)
.UpsertDefaultCollectionsAsync(
policyUpdate.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3),
_defaultUserCollectionName);
}
private static IEnumerable<object?[]> WhenDefaultCollectionsDoesNotExistTestCases()
{
yield return [new OrganizationModelOwnershipPolicyModel(null)];
yield return [new OrganizationModelOwnershipPolicyModel("")];
yield return [new OrganizationModelOwnershipPolicyModel(" ")];
yield return [new EmptyMetadataModel()];
}
[Theory]
[BitMemberAutoData(nameof(WhenDefaultCollectionsDoesNotExistTestCases))]
public async Task ExecuteSideEffectsAsync_WhenDefaultCollectionNameIsInvalid_DoesNothing(
IPolicyMetadataModel metadata,
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
policyUpdate.Enabled = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, metadata);
// Act
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive()
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
}
private static IPolicyRepository ArrangePolicyRepository(IEnumerable<OrganizationPolicyDetails> policyDetails)
{
var policyRepository = Substitute.For<IPolicyRepository>();
policyRepository
.GetPolicyDetailsByOrganizationIdAsync(Arg.Any<Guid>(), PolicyType.OrganizationDataOwnership)
.Returns(policyDetails);
return policyRepository;
}
private static OrganizationDataOwnershipPolicyValidator ArrangeSut(
OrganizationDataOwnershipPolicyRequirementFactory factory,
IPolicyRepository policyRepository,
ICollectionRepository collectionRepository)
{
var featureService = Substitute.For<IFeatureService>();
featureService
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService);
return sut;
}
}

View File

@@ -0,0 +1,172 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
[SutProviderCustomize]
public class OrganizationPolicyValidatorTests
{
[Theory, BitAutoData]
public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithNoFactory_ThrowsNotImplementedException(
Guid organizationId,
SutProvider<TestOrganizationPolicyValidator> sutProvider)
{
// Arrange
var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), []);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotImplementedException>(() =>
sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(
organizationId, PolicyType.TwoFactorAuthentication));
Assert.Contains("No Requirement Factory found for", exception.Message);
}
[Theory, BitAutoData]
public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithMultipleUsers_GroupsByUserId(
Guid organizationId,
Guid userId1,
Guid userId2,
SutProvider<TestOrganizationPolicyValidator> sutProvider)
{
// Arrange
var policyDetails = new List<OrganizationPolicyDetails>
{
new() { UserId = userId1, OrganizationId = organizationId },
new() { UserId = userId1, OrganizationId = Guid.NewGuid() },
new() { UserId = userId2, OrganizationId = organizationId }
};
var factory = Substitute.For<IPolicyRequirementFactory<TestPolicyRequirement>>();
factory.Create(Arg.Any<IEnumerable<PolicyDetails>>()).Returns(new TestPolicyRequirement());
factory.Enforce(Arg.Any<PolicyDetails>()).Returns(true);
sutProvider.GetDependency<IPolicyRepository>()
.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication)
.Returns(policyDetails);
var factories = new List<IPolicyRequirementFactory<IPolicyRequirement>> { factory };
var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), factories);
// Act
var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(
organizationId, PolicyType.TwoFactorAuthentication);
// Assert
Assert.Equal(2, result.Count());
factory.Received(2).Create(Arg.Any<IEnumerable<OrganizationPolicyDetails>>());
factory.Received(1).Create(Arg.Is<IEnumerable<OrganizationPolicyDetails>>(
results => results.Count() == 1 && results.First().UserId == userId2));
factory.Received(1).Create(Arg.Is<IEnumerable<OrganizationPolicyDetails>>(
results => results.Count() == 2 && results.First().UserId == userId1));
}
[Theory, BitAutoData]
public async Task GetUserPolicyRequirementsByOrganizationIdAsync_ShouldEnforceFilters(
Guid organizationId,
Guid userId,
SutProvider<TestOrganizationPolicyValidator> sutProvider)
{
// Arrange
var adminUser = new OrganizationPolicyDetails()
{
UserId = userId,
OrganizationId = organizationId,
OrganizationUserType = OrganizationUserType.Admin
};
var user = new OrganizationPolicyDetails()
{
UserId = userId,
OrganizationId = organizationId,
OrganizationUserType = OrganizationUserType.User
};
var policyDetails = new List<OrganizationPolicyDetails>
{
adminUser,
user
};
sutProvider.GetDependency<IPolicyRepository>()
.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication)
.Returns(policyDetails);
var factory = Substitute.For<IPolicyRequirementFactory<TestPolicyRequirement>>();
factory.Create(Arg.Any<IEnumerable<PolicyDetails>>()).Returns(new TestPolicyRequirement());
factory.Enforce(Arg.Is<PolicyDetails>(p => p.OrganizationUserType == OrganizationUserType.Admin))
.Returns(true);
factory.Enforce(Arg.Is<PolicyDetails>(p => p.OrganizationUserType == OrganizationUserType.User))
.Returns(false);
var factories = new List<IPolicyRequirementFactory<IPolicyRequirement>> { factory };
var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), factories);
// Act
var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(
organizationId, PolicyType.TwoFactorAuthentication);
// Assert
Assert.Single(result);
factory.Received(1).Create(Arg.Is<IEnumerable<PolicyDetails>>(policies =>
policies.Count() == 1 && policies.First().OrganizationUserType == OrganizationUserType.Admin));
factory.Received(1).Enforce(Arg.Is<PolicyDetails>(p => ReferenceEquals(p, adminUser)));
factory.Received(1).Enforce(Arg.Is<PolicyDetails>(p => ReferenceEquals(p, user)));
factory.Received(2).Enforce(Arg.Any<OrganizationPolicyDetails>());
}
[Theory, BitAutoData]
public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithEmptyPolicyDetails_ReturnsEmptyCollection(
Guid organizationId,
SutProvider<TestOrganizationPolicyValidator> sutProvider)
{
// Arrange
var factory = Substitute.For<IPolicyRequirementFactory<TestPolicyRequirement>>();
sutProvider.GetDependency<IPolicyRepository>()
.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication)
.Returns(new List<OrganizationPolicyDetails>());
var factories = new List<IPolicyRequirementFactory<IPolicyRequirement>> { factory };
var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), factories);
// Act
var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(
organizationId, PolicyType.TwoFactorAuthentication);
// Assert
Assert.Empty(result);
factory.DidNotReceive().Create(Arg.Any<IEnumerable<PolicyDetails>>());
}
}
public class TestOrganizationPolicyValidator : OrganizationPolicyValidator
{
public TestOrganizationPolicyValidator(
IPolicyRepository policyRepository,
IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>>? factories = null)
: base(policyRepository, factories ?? [])
{
}
public async Task<IEnumerable<T>> TestGetUserPolicyRequirementsByOrganizationIdAsync<T>(Guid organizationId, PolicyType policyType)
where T : IPolicyRequirement
{
return await GetUserPolicyRequirementsByOrganizationIdAsync<T>(organizationId, policyType);
}
}
public class TestPolicyRequirement : IPolicyRequirement
{
}

View File

@@ -94,8 +94,8 @@ public class SavePolicyCommandTests
Substitute.For<IEventService>(),
Substitute.For<IPolicyRepository>(),
[new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()],
Substitute.For<TimeProvider>()
));
Substitute.For<TimeProvider>(),
Substitute.For<IPostSavePolicySideEffect>()));
Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message);
}
@@ -281,6 +281,85 @@ public class SavePolicyCommandTests
await AssertPolicyNotSavedAsync(sutProvider);
}
[Theory, BitAutoData]
public async Task VNextSaveAsync_OrganizationDataOwnershipPolicy_ExecutesPostSaveSideEffects(
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy)
{
// Arrange
var sutProvider = SutProviderFactory();
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
.Returns(currentPolicy);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>()
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns([currentPolicy]);
// Act
var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);
// Assert
await sutProvider.GetDependency<IPolicyRepository>()
.Received(1)
.UpsertAsync(result);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogPolicyEventAsync(result, EventType.Policy_Updated);
await sutProvider.GetDependency<IPostSavePolicySideEffect>()
.Received(1)
.ExecuteSideEffectsAsync(savePolicyModel, result, currentPolicy);
}
[Theory]
[BitAutoData(PolicyType.SingleOrg)]
[BitAutoData(PolicyType.TwoFactorAuthentication)]
public async Task VNextSaveAsync_NonOrganizationDataOwnershipPolicy_DoesNotExecutePostSaveSideEffects(
PolicyType policyType,
Policy currentPolicy,
[PolicyUpdate] PolicyUpdate policyUpdate)
{
// Arrange
policyUpdate.Type = policyType;
currentPolicy.Type = policyType;
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
var sutProvider = SutProviderFactory();
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
.Returns(currentPolicy);
ArrangeOrganization(sutProvider, policyUpdate);
sutProvider.GetDependency<IPolicyRepository>()
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns([currentPolicy]);
// Act
var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);
// Assert
await sutProvider.GetDependency<IPolicyRepository>()
.Received(1)
.UpsertAsync(result);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogPolicyEventAsync(result, EventType.Policy_Updated);
await sutProvider.GetDependency<IPostSavePolicySideEffect>()
.DidNotReceiveWithAnyArgs()
.ExecuteSideEffectsAsync(default!, default!, default!);
}
/// <summary>
/// Returns a new SutProvider with the PolicyValidators registered in the Sut.
/// </summary>
@@ -289,6 +368,7 @@ public class SavePolicyCommandTests
return new SutProvider<SavePolicyCommand>()
.WithFakeTimeProvider()
.SetDependency(policyValidators ?? [])
.SetDependency(Substitute.For<IPostSavePolicySideEffect>())
.Create();
}

View File

@@ -0,0 +1,157 @@
#nullable enable
using System.Net;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Bit.Test.Common.MockedHttpClient;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class DatadogIntegrationHandlerTests
{
private readonly MockedHttpMessageHandler _handler;
private readonly HttpClient _httpClient;
private const string _apiKey = "AUTH_TOKEN";
private static readonly Uri _datadogUri = new Uri("https://localhost");
public DatadogIntegrationHandlerTests()
{
_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<DatadogIntegrationHandler> GetSutProvider()
{
var clientFactory = Substitute.For<IHttpClientFactory>();
clientFactory.CreateClient(DatadogIntegrationHandler.HttpClientName).Returns(_httpClient);
return new SutProvider<DatadogIntegrationHandler>()
.SetDependency(clientFactory)
.WithFakeTimeProvider()
.Create();
}
[Theory, BitAutoData]
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
var result = await sutProvider.Sut.HandleAsync(message);
Assert.True(result.Success);
Assert.Equal(result.Message, message);
Assert.Empty(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(DatadogIntegrationHandler.HttpClientName))
);
Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0];
Assert.NotNull(request);
Assert.NotNull(request.Content);
var returned = await request.Content.ReadAsStringAsync();
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal(_apiKey, request.Headers.GetValues("DD-API-KEY").Single());
Assert.Equal(_datadogUri, request.RequestUri);
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
}
[Theory, BitAutoData]
public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
var retryAfter = now.AddSeconds(60);
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
_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.Equal(retryAfter, result.DelayUntilDate.Value);
Assert.Equal("Too Many Requests", result.FailureReason);
}
[Theory, BitAutoData]
public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
var retryAfter = now.AddSeconds(60);
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TooManyRequests)
.WithHeader("Retry-After", retryAfter.ToString("r"))
.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.Equal(retryAfter, result.DelayUntilDate.Value);
Assert.Equal("Too Many Requests", result.FailureReason);
}
[Theory, BitAutoData]
public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
_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<DatadogIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
_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,6 +1,7 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
@@ -27,7 +28,6 @@ using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Fakes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using NSubstitute.ReceivedExtensions;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
@@ -42,8 +42,6 @@ public class OrganizationServiceTests
{
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
[Theory]
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize, BitAutoData]
@@ -1229,6 +1227,109 @@ public class OrganizationServiceTests
.GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));
}
[Theory]
[BitAutoData(false, true, false, true)]
[BitAutoData(true, false, true, false)]
public async Task UpdateCollectionManagementSettingsAsync_WhenSettingsChanged_LogsSpecificEvents(
bool newLimitCollectionCreation,
bool newLimitCollectionDeletion,
bool newLimitItemDeletion,
bool newAllowAdminAccessToAllCollectionItems,
Organization existingOrganization, SutProvider<OrganizationService> sutProvider)
{
// Arrange
existingOrganization.LimitCollectionCreation = false;
existingOrganization.LimitCollectionDeletion = false;
existingOrganization.LimitItemDeletion = false;
existingOrganization.AllowAdminAccessToAllCollectionItems = false;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(existingOrganization.Id)
.Returns(existingOrganization);
var settings = new OrganizationCollectionManagementSettings
{
LimitCollectionCreation = newLimitCollectionCreation,
LimitCollectionDeletion = newLimitCollectionDeletion,
LimitItemDeletion = newLimitItemDeletion,
AllowAdminAccessToAllCollectionItems = newAllowAdminAccessToAllCollectionItems
};
// Act
await sutProvider.Sut.UpdateCollectionManagementSettingsAsync(existingOrganization.Id, settings);
// Assert
var eventService = sutProvider.GetDependency<IEventService>();
if (newLimitCollectionCreation)
{
await eventService.Received(1).LogOrganizationEventAsync(
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled));
}
else
{
await eventService.DidNotReceive().LogOrganizationEventAsync(
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled));
}
if (newLimitCollectionDeletion)
{
await eventService.Received(1).LogOrganizationEventAsync(
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled));
}
else
{
await eventService.DidNotReceive().LogOrganizationEventAsync(
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled));
}
if (newLimitItemDeletion)
{
await eventService.Received(1).LogOrganizationEventAsync(
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitItemDeletionEnabled));
}
else
{
await eventService.DidNotReceive().LogOrganizationEventAsync(
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitItemDeletionEnabled));
}
if (newAllowAdminAccessToAllCollectionItems)
{
await eventService.Received(1).LogOrganizationEventAsync(
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled));
}
else
{
await eventService.DidNotReceive().LogOrganizationEventAsync(
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled));
}
}
[Theory, BitAutoData]
public async Task UpdateCollectionManagementSettingsAsync_WhenOrganizationNotFound_ThrowsNotFoundException(
Guid organizationId, OrganizationCollectionManagementSettings settings, SutProvider<OrganizationService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organizationId)
.Returns((Organization)null);
// Act/Assert
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateCollectionManagementSettingsAsync(organizationId, settings));
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1)
.GetByIdAsync(organizationId);
}
// Must set real guids in order for dictionary of guids to not throw aggregate exceptions
private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository)
{

View File

@@ -51,6 +51,7 @@ public class WebhookIntegrationHandlerTests
Assert.True(result.Success);
Assert.Equal(result.Message, message);
Assert.Empty(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
@@ -59,6 +60,7 @@ public class WebhookIntegrationHandlerTests
Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0];
Assert.NotNull(request);
Assert.NotNull(request.Content);
var returned = await request.Content.ReadAsStringAsync();
Assert.Equal(HttpMethod.Post, request.Method);
@@ -77,6 +79,7 @@ public class WebhookIntegrationHandlerTests
Assert.True(result.Success);
Assert.Equal(result.Message, message);
Assert.Empty(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
@@ -85,6 +88,7 @@ public class WebhookIntegrationHandlerTests
Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0];
Assert.NotNull(request);
Assert.NotNull(request.Content);
var returned = await request.Content.ReadAsStringAsync();
Assert.Equal(HttpMethod.Post, request.Method);

View File

@@ -1,6 +1,5 @@
#nullable enable
using System.Text.Json;
using Bit.Core.AdminConsole.Utilities;
using Bit.Core.Models.Data;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -41,22 +40,12 @@ public class IntegrationTemplateProcessorTests
}
[Theory, BitAutoData]
public void ReplaceTokens_WithEventMessageToken_ReplacesWithSerializedJson(EventMessage eventMessage)
{
var template = "#EventMessage#";
var expected = $"{JsonSerializer.Serialize(eventMessage)}";
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
Assert.Equal(expected, result);
}
[Theory, BitAutoData]
public void ReplaceTokens_WithNullProperty_LeavesTokenUnchanged(EventMessage eventMessage)
public void ReplaceTokens_WithNullProperty_InsertsEmptyString(EventMessage eventMessage)
{
eventMessage.UserId = null;
var template = "Event #Type#, User (id: #UserId#).";
var expected = $"Event {eventMessage.Type}, User (id: #UserId#).";
var expected = $"Event {eventMessage.Type}, User (id: ).";
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
Assert.Equal(expected, result);

View File

@@ -1,6 +1,6 @@
using System.Security.Claims;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.UserFeatures.SendAccess;
using Bit.Core.Identity;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.SendAccess;
@@ -12,7 +12,7 @@ public class SendAccessClaimsPrincipalExtensionsTests
{
// Arrange
var guid = Guid.NewGuid();
var claims = new[] { new Claim(Claims.SendId, guid.ToString()) };
var claims = new[] { new Claim(Claims.SendAccessClaims.SendId, guid.ToString()) };
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
@@ -30,19 +30,19 @@ public class SendAccessClaimsPrincipalExtensionsTests
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => principal.GetSendId());
Assert.Equal("Send ID claim not found.", ex.Message);
Assert.Equal("send_id claim not found.", ex.Message);
}
[Fact]
public void GetSendId_ThrowsInvalidOperationException_WhenClaimValueIsInvalid()
{
// Arrange
var claims = new[] { new Claim(Claims.SendId, "not-a-guid") };
var claims = new[] { new Claim(Claims.SendAccessClaims.SendId, "not-a-guid") };
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => principal.GetSendId());
Assert.Equal("Invalid Send ID claim value.", ex.Message);
Assert.Equal("Invalid send_id claim value.", ex.Message);
}
[Fact]

View File

@@ -0,0 +1,394 @@
using Bit.Core.Billing.Extensions;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Extensions;
public class InvoiceExtensionsTests
{
private static Invoice CreateInvoiceWithLines(params InvoiceLineItem[] lineItems)
{
return new Invoice
{
Lines = new StripeList<InvoiceLineItem>
{
Data = lineItems?.ToList() ?? new List<InvoiceLineItem>()
}
};
}
#region FormatForProvider Tests
[Fact]
public void FormatForProvider_NullLines_ReturnsEmptyList()
{
// Arrange
var invoice = new Invoice
{
Lines = null
};
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void FormatForProvider_EmptyLines_ReturnsEmptyList()
{
// Arrange
var invoice = CreateInvoiceWithLines();
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void FormatForProvider_NullLineItem_SkipsNullLine()
{
// Arrange
var invoice = CreateInvoiceWithLines(null);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void FormatForProvider_LineWithNullDescription_SkipsLine()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem { Description = null, Quantity = 1, Amount = 1000 }
);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void FormatForProvider_ProviderPortalTeams_FormatsCorrectly()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Provider Portal - Teams (at $6.00 / month)",
Quantity = 5,
Amount = 3000
}
);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("5 × Manage service provider (at $6.00 / month)", result[0]);
}
[Fact]
public void FormatForProvider_ProviderPortalEnterprise_FormatsCorrectly()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Provider Portal - Enterprise (at $4.00 / month)",
Quantity = 10,
Amount = 4000
}
);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("10 × Manage service provider (at $4.00 / month)", result[0]);
}
[Fact]
public void FormatForProvider_ProviderPortalWithoutPriceInfo_FormatsWithoutPrice()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Provider Portal - Teams",
Quantity = 3,
Amount = 1800
}
);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("3 × Manage service provider ", result[0]);
}
[Fact]
public void FormatForProvider_BusinessUnitPortalEnterprise_FormatsCorrectly()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Business Unit Portal - Enterprise (at $5.00 / month)",
Quantity = 8,
Amount = 4000
}
);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("8 × Manage service provider (at $5.00 / month)", result[0]);
}
[Fact]
public void FormatForProvider_BusinessUnitPortalGeneric_FormatsCorrectly()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Business Unit Portal (at $3.00 / month)",
Quantity = 2,
Amount = 600
}
);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("2 × Manage service provider (at $3.00 / month)", result[0]);
}
[Fact]
public void FormatForProvider_TaxLineWithPriceInfo_FormatsCorrectly()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Tax (at $2.00 / month)",
Quantity = 1,
Amount = 200
}
);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("1 × Tax (at $2.00 / month)", result[0]);
}
[Fact]
public void FormatForProvider_TaxLineWithoutPriceInfo_CalculatesPrice()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Tax",
Quantity = 2,
Amount = 400 // $4.00 total, $2.00 per item
}
);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("2 × Tax (at $2.00 / month)", result[0]);
}
[Fact]
public void FormatForProvider_TaxLineWithZeroQuantity_DoesNotCalculatePrice()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Tax",
Quantity = 0,
Amount = 200
}
);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("0 × Tax ", result[0]);
}
[Fact]
public void FormatForProvider_OtherLineItem_ReturnsAsIs()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Some other service",
Quantity = 1,
Amount = 1000
}
);
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("Some other service", result[0]);
}
[Fact]
public void FormatForProvider_InvoiceLevelTax_AddsToResult()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Provider Portal - Teams",
Quantity = 1,
Amount = 600
}
);
invoice.Tax = 120; // $1.20 in cents
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Equal(2, result.Count);
Assert.Equal("1 × Manage service provider ", result[0]);
Assert.Equal("1 × Tax (at $1.20 / month)", result[1]);
}
[Fact]
public void FormatForProvider_NoInvoiceLevelTax_DoesNotAddTax()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Provider Portal - Teams",
Quantity = 1,
Amount = 600
}
);
invoice.Tax = null;
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("1 × Manage service provider ", result[0]);
}
[Fact]
public void FormatForProvider_ZeroInvoiceLevelTax_DoesNotAddTax()
{
// Arrange
var invoice = CreateInvoiceWithLines(
new InvoiceLineItem
{
Description = "Provider Portal - Teams",
Quantity = 1,
Amount = 600
}
);
invoice.Tax = 0;
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Single(result);
Assert.Equal("1 × Manage service provider ", result[0]);
}
[Fact]
public void FormatForProvider_ComplexScenario_HandlesAllLineTypes()
{
// Arrange
var lineItems = new StripeList<InvoiceLineItem>();
lineItems.Data = new List<InvoiceLineItem>
{
new InvoiceLineItem
{
Description = "Provider Portal - Teams (at $6.00 / month)", Quantity = 5, Amount = 3000
},
new InvoiceLineItem
{
Description = "Provider Portal - Enterprise (at $4.00 / month)", Quantity = 10, Amount = 4000
},
new InvoiceLineItem { Description = "Tax", Quantity = 1, Amount = 800 },
new InvoiceLineItem { Description = "Custom Service", Quantity = 2, Amount = 2000 }
};
var invoice = new Invoice
{
Lines = lineItems,
Tax = 200 // Additional $2.00 tax
};
var subscription = new Subscription();
// Act
var result = invoice.FormatForProvider(subscription);
// Assert
Assert.Equal(5, result.Count);
Assert.Equal("5 × Manage service provider (at $6.00 / month)", result[0]);
Assert.Equal("10 × Manage service provider (at $4.00 / month)", result[1]);
Assert.Equal("1 × Tax (at $8.00 / month)", result[2]);
Assert.Equal("Custom Service", result[3]);
Assert.Equal("1 × Tax (at $2.00 / month)", result[4]);
}
#endregion
}

View File

@@ -71,7 +71,7 @@ public class GetOrganizationWarningsQueryTests
});
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns((string?)null);
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(organization.Id).Returns((string?)null);
var response = await sutProvider.Sut.Run(organization);
@@ -109,7 +109,7 @@ public class GetOrganizationWarningsQueryTests
});
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns(setupIntentId);
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(organization.Id).Returns(setupIntentId);
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(
options => options.Expand.Contains("payment_method"))).Returns(new SetupIntent
{

View File

@@ -74,7 +74,10 @@ public class UpdatePaymentMethodCommandTests
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
{
HostedVerificationUrl = "https://example.com"
}
},
Status = "requires_action"
};
@@ -95,7 +98,7 @@ public class UpdatePaymentMethodCommandTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.False(maskedBankAccount.Verified);
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
}
@@ -133,7 +136,10 @@ public class UpdatePaymentMethodCommandTests
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
{
HostedVerificationUrl = "https://example.com"
}
},
Status = "requires_action"
};
@@ -154,7 +160,7 @@ public class UpdatePaymentMethodCommandTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.False(maskedBankAccount.Verified);
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
await _subscriberService.Received(1).CreateStripeCustomer(organization);
@@ -199,7 +205,10 @@ public class UpdatePaymentMethodCommandTests
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
{
HostedVerificationUrl = "https://example.com"
}
},
Status = "requires_action"
};
@@ -220,7 +229,7 @@ public class UpdatePaymentMethodCommandTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.False(maskedBankAccount.Verified);
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>

View File

@@ -1,81 +0,0 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Extensions;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Payment.Commands;
public class VerifyBankAccountCommandTests
{
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
private readonly VerifyBankAccountCommand _command;
public VerifyBankAccountCommandTests()
{
_command = new VerifyBankAccountCommand(
Substitute.For<ILogger<VerifyBankAccountCommand>>(),
_setupIntentCache,
_stripeAdapter);
}
[Fact]
public async Task Run_MakesCorrectInvocations_ReturnsMaskedBankAccount()
{
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewayCustomerId = "cus_123"
};
const string setupIntentId = "seti_123";
_setupIntentCache.Get(organization.Id).Returns(setupIntentId);
var setupIntent = new SetupIntent
{
Id = setupIntentId,
PaymentMethodId = "pm_123",
PaymentMethod =
new PaymentMethod
{
Id = "pm_123",
Type = "us_bank_account",
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
},
Status = "requires_action"
};
_stripeAdapter.SetupIntentGet(setupIntentId,
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method"))).Returns(setupIntent);
_stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
Arg.Is<PaymentMethodAttachOptions>(options => options.Customer == organization.GatewayCustomerId))
.Returns(setupIntent.PaymentMethod);
var result = await _command.Run(organization, "DESCRIPTOR_CODE");
Assert.True(result.IsT0);
var maskedPaymentMethod = result.AsT0;
Assert.True(maskedPaymentMethod.IsT0);
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.True(maskedBankAccount.Verified);
await _stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id,
Arg.Is<SetupIntentVerifyMicrodepositsOptions>(options => options.DescriptorCode == "DESCRIPTOR_CODE"));
await _stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options => options.InvoiceSettings.DefaultPaymentMethod == setupIntent.PaymentMethodId));
}
}

View File

@@ -13,7 +13,7 @@ public class MaskedPaymentMethodTests
{
BankName = "Chase",
Last4 = "9999",
Verified = true
HostedVerificationUrl = "https://example.com"
};
var json = JsonSerializer.Serialize(input);
@@ -32,7 +32,7 @@ public class MaskedPaymentMethodTests
{
BankName = "Chase",
Last4 = "9999",
Verified = true
HostedVerificationUrl = "https://example.com"
};
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

View File

@@ -108,7 +108,7 @@ public class GetPaymentMethodQueryTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.True(maskedBankAccount.Verified);
Assert.Null(maskedBankAccount.HostedVerificationUrl);
}
[Fact]
@@ -142,7 +142,7 @@ public class GetPaymentMethodQueryTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.True(maskedBankAccount.Verified);
Assert.Null(maskedBankAccount.HostedVerificationUrl);
}
[Fact]
@@ -163,7 +163,7 @@ public class GetPaymentMethodQueryTests
Arg.Is<CustomerGetOptions>(options =>
options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer);
_setupIntentCache.Get(organization.Id).Returns("seti_123");
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
_stripeAdapter
.SetupIntentGet("seti_123",
@@ -177,7 +177,10 @@ public class GetPaymentMethodQueryTests
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
{
HostedVerificationUrl = "https://example.com"
}
},
Status = "requires_action"
});
@@ -189,7 +192,7 @@ public class GetPaymentMethodQueryTests
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.False(maskedBankAccount.Verified);
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
}
[Fact]

View File

@@ -0,0 +1,477 @@
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture.Attributes;
using Braintree;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Stripe;
using Xunit;
using Address = Stripe.Address;
using StripeCustomer = Stripe.Customer;
using StripeSubscription = Stripe.Subscription;
namespace Bit.Core.Test.Billing.Premium.Commands;
public class CreatePremiumCloudHostedSubscriptionCommandTests
{
private readonly IBraintreeGateway _braintreeGateway = Substitute.For<IBraintreeGateway>();
private readonly IGlobalSettings _globalSettings = Substitute.For<IGlobalSettings>();
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
private readonly IUserService _userService = Substitute.For<IUserService>();
private readonly IPushNotificationService _pushNotificationService = Substitute.For<IPushNotificationService>();
private readonly CreatePremiumCloudHostedSubscriptionCommand _command;
public CreatePremiumCloudHostedSubscriptionCommandTests()
{
var baseServiceUri = Substitute.For<IBaseServiceUriSettings>();
baseServiceUri.CloudRegion.Returns("US");
_globalSettings.BaseServiceUri.Returns(baseServiceUri);
_command = new CreatePremiumCloudHostedSubscriptionCommand(
_braintreeGateway,
_globalSettings,
_setupIntentCache,
_stripeAdapter,
_subscriberService,
_userService,
_pushNotificationService,
Substitute.For<ILogger<CreatePremiumCloudHostedSubscriptionCommand>>());
}
[Theory, BitAutoData]
public async Task Run_UserAlreadyPremium_ReturnsBadRequest(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = true;
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Equal("Already a premium user.", badRequest.Response);
}
[Theory, BitAutoData]
public async Task Run_NegativeStorageAmount_ReturnsBadRequest(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, -1);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Equal("Additional storage must be greater than 0.", badRequest.Response);
}
[Theory, BitAutoData]
public async Task Run_ValidPaymentMethodTypes_BankAccount_Success(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = null; // Ensure no existing customer ID
user.Email = "test@example.com";
paymentMethod.Type = TokenizablePaymentMethodType.BankAccount;
paymentMethod.Token = "bank_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "cust_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
var mockInvoice = Substitute.For<Invoice>();
var mockSetupIntent = Substitute.For<SetupIntent>();
mockSetupIntent.Id = "seti_123";
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.SetupIntentList(Arg.Any<SetupIntentListOptions>()).Returns(Task.FromResult(new List<SetupIntent> { mockSetupIntent }));
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _userService.Received(1).SaveUserAsync(user);
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
}
[Theory, BitAutoData]
public async Task Run_ValidPaymentMethodTypes_Card_Success(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = null;
user.Email = "test@example.com";
paymentMethod.Type = TokenizablePaymentMethodType.Card;
paymentMethod.Token = "card_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "cust_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _userService.Received(1).SaveUserAsync(user);
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
}
[Theory, BitAutoData]
public async Task Run_ValidPaymentMethodTypes_PayPal_Success(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = null;
user.Email = "test@example.com";
paymentMethod.Type = TokenizablePaymentMethodType.PayPal;
paymentMethod.Token = "paypal_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "cust_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns("bt_customer_123");
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _subscriberService.Received(1).CreateBraintreeCustomer(user, paymentMethod.Token);
await _userService.Received(1).SaveUserAsync(user);
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
}
[Theory, BitAutoData]
public async Task Run_ValidRequestWithAdditionalStorage_Success(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = null;
user.Email = "test@example.com";
paymentMethod.Type = TokenizablePaymentMethodType.Card;
paymentMethod.Token = "card_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
const short additionalStorage = 2;
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "cust_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, additionalStorage);
// Assert
Assert.True(result.IsT0);
Assert.True(user.Premium);
Assert.Equal((short)(1 + additionalStorage), user.MaxStorageGb);
Assert.NotNull(user.LicenseKey);
Assert.Equal(20, user.LicenseKey.Length);
Assert.NotEqual(default, user.RevisionDate);
await _userService.Received(1).SaveUserAsync(user);
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
}
[Theory, BitAutoData]
public async Task Run_UserHasExistingGatewayCustomerId_UsesExistingCustomer(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = "existing_customer_123";
paymentMethod.Type = TokenizablePaymentMethodType.Card;
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "existing_customer_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
var mockInvoice = Substitute.For<Invoice>();
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT0);
await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());
await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
}
[Theory, BitAutoData]
public async Task Run_PayPalWithIncompleteSubscription_SetsPremiumTrue(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = null;
user.Email = "test@example.com";
user.PremiumExpirationDate = null;
paymentMethod.Type = TokenizablePaymentMethodType.PayPal;
paymentMethod.Token = "paypal_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "cust_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "incomplete";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns("bt_customer_123");
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT0);
Assert.True(user.Premium);
Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate);
}
[Theory, BitAutoData]
public async Task Run_NonPayPalWithActiveSubscription_SetsPremiumTrue(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = null;
user.Email = "test@example.com";
paymentMethod.Type = TokenizablePaymentMethodType.Card;
paymentMethod.Token = "card_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "cust_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT0);
Assert.True(user.Premium);
Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate);
}
[Theory, BitAutoData]
public async Task Run_SubscriptionStatusDoesNotMatchPatterns_DoesNotSetPremium(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = null;
user.Email = "test@example.com";
user.PremiumExpirationDate = null;
paymentMethod.Type = TokenizablePaymentMethodType.PayPal;
paymentMethod.Token = "paypal_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "cust_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active"; // PayPal + active doesn't match pattern
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns("bt_customer_123");
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT0);
Assert.False(user.Premium);
Assert.Null(user.PremiumExpirationDate);
}
[Theory, BitAutoData]
public async Task Run_BankAccountWithNoSetupIntentFound_ReturnsUnhandled(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = null;
user.Email = "test@example.com";
paymentMethod.Type = TokenizablePaymentMethodType.BankAccount;
paymentMethod.Token = "bank_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "cust_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "incomplete";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_stripeAdapter.SetupIntentList(Arg.Any<SetupIntentListOptions>())
.Returns(Task.FromResult(new List<SetupIntent>())); // Empty list - no setup intent found
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.Equal("Something went wrong with your request. Please contact support for assistance.", unhandled.Response);
}
}

View File

@@ -0,0 +1,199 @@
using System.Security.Claims;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Billing.Premium.Commands;
public class CreatePremiumSelfHostedSubscriptionCommandTests
{
private readonly ILicensingService _licensingService = Substitute.For<ILicensingService>();
private readonly IUserService _userService = Substitute.For<IUserService>();
private readonly IPushNotificationService _pushNotificationService = Substitute.For<IPushNotificationService>();
private readonly CreatePremiumSelfHostedSubscriptionCommand _command;
public CreatePremiumSelfHostedSubscriptionCommandTests()
{
_command = new CreatePremiumSelfHostedSubscriptionCommand(
_licensingService,
_userService,
_pushNotificationService,
Substitute.For<ILogger<CreatePremiumSelfHostedSubscriptionCommand>>());
}
[Fact]
public async Task Run_UserAlreadyPremium_ReturnsBadRequest()
{
// Arrange
var user = new User
{
Id = Guid.NewGuid(),
Premium = true
};
var license = new UserLicense
{
LicenseKey = "test_key",
Expires = DateTime.UtcNow.AddYears(1)
};
// Act
var result = await _command.Run(user, license);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Equal("Already a premium user.", badRequest.Response);
}
[Fact]
public async Task Run_InvalidLicense_ReturnsBadRequest()
{
// Arrange
var user = new User
{
Id = Guid.NewGuid(),
Premium = false
};
var license = new UserLicense
{
LicenseKey = "invalid_key",
Expires = DateTime.UtcNow.AddYears(1)
};
_licensingService.VerifyLicense(license).Returns(false);
// Act
var result = await _command.Run(user, license);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Equal("Invalid license.", badRequest.Response);
}
[Fact]
public async Task Run_LicenseCannotBeUsed_EmailNotVerified_ReturnsBadRequest()
{
// Arrange
var user = new User
{
Id = Guid.NewGuid(),
Premium = false,
Email = "test@example.com",
EmailVerified = false
};
var license = new UserLicense
{
LicenseKey = "test_key",
Expires = DateTime.UtcNow.AddYears(1),
Token = "valid_token"
};
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim("Email", "test@example.com")
}));
_licensingService.VerifyLicense(license).Returns(true);
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
// Act
var result = await _command.Run(user, license);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Contains("The user's email is not verified.", badRequest.Response);
}
[Fact]
public async Task Run_LicenseCannotBeUsed_EmailMismatch_ReturnsBadRequest()
{
// Arrange
var user = new User
{
Id = Guid.NewGuid(),
Premium = false,
Email = "user@example.com",
EmailVerified = true
};
var license = new UserLicense
{
LicenseKey = "test_key",
Expires = DateTime.UtcNow.AddYears(1),
Token = "valid_token"
};
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim("Email", "license@example.com")
}));
_licensingService.VerifyLicense(license).Returns(true);
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
// Act
var result = await _command.Run(user, license);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Contains("The user's email does not match the license email.", badRequest.Response);
}
[Fact]
public async Task Run_ValidRequest_Success()
{
// Arrange
var userId = Guid.NewGuid();
var user = new User
{
Id = userId,
Premium = false,
Email = "test@example.com",
EmailVerified = true
};
var license = new UserLicense
{
LicenseKey = "test_key_12345",
Expires = DateTime.UtcNow.AddYears(1),
Token = "valid_token"
};
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim("Email", "test@example.com")
}));
_licensingService.VerifyLicense(license).Returns(true);
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
// Act
var result = await _command.Run(user, license);
// Assert
Assert.True(result.IsT0);
// Verify user was updated correctly
Assert.True(user.Premium);
Assert.NotNull(user.LicenseKey);
Assert.Equal(license.LicenseKey, user.LicenseKey);
Assert.NotEqual(default, user.RevisionDate);
// Verify services were called
await _licensingService.Received(1).WriteUserLicenseAsync(user, license);
await _userService.Received(1).SaveUserAsync(user);
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
}
}

View File

@@ -1,8 +1,10 @@
using System.Text.Json;
using AutoFixture;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Settings;
using Bit.Core.Test.Billing.AutoFixture;
using Bit.Test.Common.AutoFixture;
@@ -16,6 +18,8 @@ public class LicensingServiceTests
{
private static string licenseFilePath(Guid orgId) =>
Path.Combine(OrganizationLicenseDirectory.Value, $"{orgId}.json");
private static string userLicenseFilePath(Guid userId) =>
Path.Combine(UserLicenseDirectory.Value, $"{userId}.json");
private static string LicenseDirectory => Path.GetDirectoryName(OrganizationLicenseDirectory.Value);
private static Lazy<string> OrganizationLicenseDirectory => new(() =>
{
@@ -26,6 +30,15 @@ public class LicensingServiceTests
}
return directory;
});
private static Lazy<string> UserLicenseDirectory => new(() =>
{
var directory = Path.Combine(Path.GetTempPath(), "user");
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
return directory;
});
public static SutProvider<LicensingService> GetSutProvider()
{
@@ -57,4 +70,66 @@ public class LicensingServiceTests
Directory.Delete(OrganizationLicenseDirectory.Value, true);
}
}
[Theory, BitAutoData]
public async Task WriteUserLicense_CreatesFileWithCorrectContent(User user, UserLicense license)
{
// Arrange
var sutProvider = GetSutProvider();
var expectedFilePath = userLicenseFilePath(user.Id);
try
{
// Act
await sutProvider.Sut.WriteUserLicenseAsync(user, license);
// Assert
Assert.True(File.Exists(expectedFilePath));
var fileContent = await File.ReadAllTextAsync(expectedFilePath);
var actualLicense = JsonSerializer.Deserialize<UserLicense>(fileContent);
Assert.Equal(license.LicenseKey, actualLicense.LicenseKey);
Assert.Equal(license.Id, actualLicense.Id);
Assert.Equal(license.Expires, actualLicense.Expires);
}
finally
{
// Cleanup
if (Directory.Exists(UserLicenseDirectory.Value))
{
Directory.Delete(UserLicenseDirectory.Value, true);
}
}
}
[Theory, BitAutoData]
public async Task WriteUserLicense_CreatesDirectoryIfNotExists(User user, UserLicense license)
{
// Arrange
var sutProvider = GetSutProvider();
// Ensure directory doesn't exist
if (Directory.Exists(UserLicenseDirectory.Value))
{
Directory.Delete(UserLicenseDirectory.Value, true);
}
try
{
// Act
await sutProvider.Sut.WriteUserLicenseAsync(user, license);
// Assert
Assert.True(Directory.Exists(UserLicenseDirectory.Value));
Assert.True(File.Exists(userLicenseFilePath(user.Id)));
}
finally
{
// Cleanup
if (Directory.Exists(UserLicenseDirectory.Value))
{
Directory.Delete(UserLicenseDirectory.Value, true);
}
}
}
}

View File

@@ -329,13 +329,165 @@ public class SubscriberServiceTests
#endregion
#region GetPaymentMethod
[Theory, BitAutoData]
public async Task GetPaymentMethod_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));
[Theory, BitAutoData]
public async Task GetPaymentMethod_Braintree_NoDefaultPaymentMethod_ReturnsNull(
public async Task GetPaymentMethod_WithNegativeStripeAccountBalance_ReturnsCorrectAccountCreditAmount(Organization organization,
SutProvider<SubscriberService> sutProvider)
{
// Arrange
// Stripe reports balance in cents as a negative number for credit
const int stripeAccountBalance = -593; // $5.93 credit (negative cents)
const decimal creditAmount = 5.93M; // Same value in dollars
var customer = new Customer
{
Balance = stripeAccountBalance,
Subscriptions = new StripeList<Subscription>()
{
Data =
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
},
InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethod = new PaymentMethod
{
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
}
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")
&& options.Expand.Contains("subscriptions")
&& options.Expand.Contains("tax_ids")))
.Returns(customer);
// Act
var result = await sutProvider.Sut.GetPaymentMethod(organization);
// Assert
Assert.NotNull(result);
Assert.Equal(creditAmount, result.AccountCredit);
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options =>
options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method") &&
options.Expand.Contains("subscriptions") &&
options.Expand.Contains("tax_ids")));
}
[Theory, BitAutoData]
public async Task GetPaymentMethod_WithZeroStripeAccountBalance_ReturnsCorrectAccountCreditAmount(
Organization organization, SutProvider<SubscriberService> sutProvider)
{
// Arrange
const int stripeAccountBalance = 0;
var customer = new Customer
{
Balance = stripeAccountBalance,
Subscriptions = new StripeList<Subscription>()
{
Data =
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
},
InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethod = new PaymentMethod
{
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
}
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")
&& options.Expand.Contains("subscriptions")
&& options.Expand.Contains("tax_ids")))
.Returns(customer);
// Act
var result = await sutProvider.Sut.GetPaymentMethod(organization);
// Assert
Assert.NotNull(result);
Assert.Equal(0, result.AccountCredit);
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options =>
options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method") &&
options.Expand.Contains("subscriptions") &&
options.Expand.Contains("tax_ids")));
}
[Theory, BitAutoData]
public async Task GetPaymentMethod_WithPositiveStripeAccountBalance_ReturnsCorrectAccountCreditAmount(
Organization organization, SutProvider<SubscriberService> sutProvider)
{
// Arrange
const int stripeAccountBalance = 593; // $5.93 charge balance
const decimal accountBalance = -5.93M; // account balance
var customer = new Customer
{
Balance = stripeAccountBalance,
Subscriptions = new StripeList<Subscription>()
{
Data =
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
},
InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethod = new PaymentMethod
{
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
}
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")
&& options.Expand.Contains("subscriptions")
&& options.Expand.Contains("tax_ids")))
.Returns(customer);
// Act
var result = await sutProvider.Sut.GetPaymentMethod(organization);
// Assert
Assert.NotNull(result);
Assert.Equal(accountBalance, result.AccountCredit);
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options =>
options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method") &&
options.Expand.Contains("subscriptions") &&
options.Expand.Contains("tax_ids")));
}
#endregion
#region GetPaymentSource
[Theory, BitAutoData]
public async Task GetPaymentSource_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));
[Theory, BitAutoData]
public async Task GetPaymentSource_Braintree_NoDefaultPaymentMethod_ReturnsNull(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
@@ -372,7 +524,7 @@ public class SubscriberServiceTests
}
[Theory, BitAutoData]
public async Task GetPaymentMethod_Braintree_PayPalAccount_Succeeds(
public async Task GetPaymentSource_Braintree_PayPalAccount_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
@@ -421,7 +573,7 @@ public class SubscriberServiceTests
// TODO: Determine if we need to test Braintree.UsBankAccount
[Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_BankAccountPaymentMethod_Succeeds(
public async Task GetPaymentSource_Stripe_BankAccountPaymentMethod_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
@@ -455,7 +607,7 @@ public class SubscriberServiceTests
}
[Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_CardPaymentMethod_Succeeds(
public async Task GetPaymentSource_Stripe_CardPaymentMethod_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
@@ -491,43 +643,37 @@ public class SubscriberServiceTests
}
[Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_SetupIntentForBankAccount_Succeeds(
public async Task GetPaymentSource_Stripe_SetupIntentForBankAccount_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer
{
Id = provider.GatewayCustomerId
};
var customer = new Customer { Id = provider.GatewayCustomerId };
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains(
"invoice_settings.default_payment_method")))
.Returns(customer);
var setupIntent = new SetupIntent
{
Id = "setup_intent_id",
Status = "requires_action",
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
},
NextAction =
new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
},
PaymentMethod = new PaymentMethod
{
UsBankAccount = new PaymentMethodUsBankAccount
{
BankName = "Chase",
Last4 = "9999"
}
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
}
};
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntent.Id);
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id, Arg.Is<SetupIntentGetOptions>(
options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id,
Arg.Is<SetupIntentGetOptions>(options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
@@ -537,24 +683,19 @@ public class SubscriberServiceTests
}
[Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_LegacyBankAccount_Succeeds(
public async Task GetPaymentSource_Stripe_LegacyBankAccount_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer
{
DefaultSource = new BankAccount
{
Status = "verified",
BankName = "Chase",
Last4 = "9999"
}
DefaultSource = new BankAccount { Status = "verified", BankName = "Chase", Last4 = "9999" }
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains(
"invoice_settings.default_payment_method")))
.Returns(customer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
@@ -565,25 +706,19 @@ public class SubscriberServiceTests
}
[Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_LegacyCard_Succeeds(
public async Task GetPaymentSource_Stripe_LegacyCard_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
var customer = new Customer
{
DefaultSource = new Card
{
Brand = "Visa",
Last4 = "9999",
ExpMonth = 9,
ExpYear = 2028
}
DefaultSource = new Card { Brand = "Visa", Last4 = "9999", ExpMonth = 9, ExpYear = 2028 }
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains(
"invoice_settings.default_payment_method")))
.Returns(customer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
@@ -594,7 +729,7 @@ public class SubscriberServiceTests
}
[Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_LegacySourceCard_Succeeds(
public async Task GetPaymentSource_Stripe_LegacySourceCard_Succeeds(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
@@ -1185,12 +1320,6 @@ public class SubscriberServiceTests
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options => options.PaymentMethod == "TOKEN"))
.Returns([matchingSetupIntent]);
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options => options.Customer == provider.GatewayCustomerId))
.Returns([
new SetupIntent { Id = "setup_intent_2", Status = "requires_payment_method" },
new SetupIntent { Id = "setup_intent_3", Status = "succeeded" }
]);
stripeAdapter.CustomerListPaymentMethods(provider.GatewayCustomerId).Returns([
new PaymentMethod { Id = "payment_method_1" }
]);
@@ -1200,8 +1329,8 @@ public class SubscriberServiceTests
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_1");
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_2",
Arg.Is<SetupIntentCancelOptions>(options => options.CancellationReason == "abandoned"));
await stripeAdapter.DidNotReceive().SetupIntentCancel(Arg.Any<string>(),
Arg.Any<SetupIntentCancelOptions>());
await stripeAdapter.Received(1).PaymentMethodDetachAsync("payment_method_1");
@@ -1229,12 +1358,6 @@ public class SubscriberServiceTests
}
});
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options => options.Customer == provider.GatewayCustomerId))
.Returns([
new SetupIntent { Id = "setup_intent_2", Status = "requires_payment_method" },
new SetupIntent { Id = "setup_intent_3", Status = "succeeded" }
]);
stripeAdapter.CustomerListPaymentMethods(provider.GatewayCustomerId).Returns([
new PaymentMethod { Id = "payment_method_1" }
]);
@@ -1242,8 +1365,8 @@ public class SubscriberServiceTests
await sutProvider.Sut.UpdatePaymentSource(provider,
new TokenizedPaymentSource(PaymentMethodType.Card, "TOKEN"));
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_2",
Arg.Is<SetupIntentCancelOptions>(options => options.CancellationReason == "abandoned"));
await stripeAdapter.DidNotReceive().SetupIntentCancel(Arg.Any<string>(),
Arg.Any<SetupIntentCancelOptions>());
await stripeAdapter.Received(1).PaymentMethodDetachAsync("payment_method_1");
@@ -1741,7 +1864,7 @@ public class SubscriberServiceTests
PaymentMethodId = "payment_method_id"
};
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntent.Id);
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();

View File

@@ -181,7 +181,7 @@ public class PreviewTaxAmountCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.SubscriptionDetails.Items[0].Quantity == 1 &&
options.AutomaticTax.Enabled == false
options.AutomaticTax.Enabled == true
))
.Returns(expectedInvoice);
@@ -273,4 +273,269 @@ public class PreviewTaxAmountCommandTests
var badRequest = result.AsT1;
Assert.Equal("We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support for assistance.", badRequest.Response);
}
[Fact]
public async Task Run_USBased_PersonalUse_SetsAutomaticTaxEnabled()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.FamiliesAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "US",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_USBased_BusinessUse_SetsAutomaticTaxEnabled()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.EnterpriseAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "US",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.FamiliesAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "CA",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.EnterpriseAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "CA",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_USBased_PersonalUse_DoesNotSetTaxExempt()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.FamiliesAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "US",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_USBased_BusinessUse_DoesNotSetTaxExempt()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.EnterpriseAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "US",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_NonUSBased_PersonalUse_DoesNotSetTaxExempt()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.FamiliesAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "CA",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_NonUSBased_BusinessUse_SetsTaxExemptReverse()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.EnterpriseAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "CA",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse
));
Assert.True(result.IsT0);
}
}

View File

@@ -1,105 +0,0 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Billing.Tax.Services.Implementations;
using Bit.Core.Entities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Billing.Tax.Services;
[SutProviderCustomize]
public class AutomaticTaxFactoryTests
{
[BitAutoData]
[Theory]
public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenSubscriberIsUser(SutProvider<AutomaticTaxFactory> sut)
{
var parameters = new AutomaticTaxFactoryParameters(new User(), []);
var actual = await sut.Sut.CreateAsync(parameters);
Assert.IsType<PersonalUseAutomaticTaxStrategy>(actual);
}
[BitAutoData]
[Theory]
public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenSubscriberIsOrganizationWithFamiliesAnnuallyPrice(
SutProvider<AutomaticTaxFactory> sut)
{
var familiesPlan = new FamiliesPlan();
var parameters = new AutomaticTaxFactoryParameters(new Organization(), [familiesPlan.PasswordManager.StripePlanId]);
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(new FamiliesPlan());
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually2019))
.Returns(new Families2019Plan());
var actual = await sut.Sut.CreateAsync(parameters);
Assert.IsType<PersonalUseAutomaticTaxStrategy>(actual);
}
[Theory]
[BitAutoData]
public async Task CreateAsync_ReturnsBusinessUseStrategy_WhenSubscriberIsOrganizationWithBusinessUsePrice(
EnterpriseAnnually plan,
SutProvider<AutomaticTaxFactory> sut)
{
var parameters = new AutomaticTaxFactoryParameters(new Organization(), [plan.PasswordManager.StripePlanId]);
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(new FamiliesPlan());
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually2019))
.Returns(new Families2019Plan());
var actual = await sut.Sut.CreateAsync(parameters);
Assert.IsType<BusinessUseAutomaticTaxStrategy>(actual);
}
[Theory]
[BitAutoData]
public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenPlanIsMeantForPersonalUse(SutProvider<AutomaticTaxFactory> sut)
{
var parameters = new AutomaticTaxFactoryParameters(PlanType.FamiliesAnnually);
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == parameters.PlanType.Value))
.Returns(new FamiliesPlan());
var actual = await sut.Sut.CreateAsync(parameters);
Assert.IsType<PersonalUseAutomaticTaxStrategy>(actual);
}
[Theory]
[BitAutoData]
public async Task CreateAsync_ReturnsBusinessUseStrategy_WhenPlanIsMeantForBusinessUse(SutProvider<AutomaticTaxFactory> sut)
{
var parameters = new AutomaticTaxFactoryParameters(PlanType.EnterpriseAnnually);
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == parameters.PlanType.Value))
.Returns(new EnterprisePlan(true));
var actual = await sut.Sut.CreateAsync(parameters);
Assert.IsType<BusinessUseAutomaticTaxStrategy>(actual);
}
public record EnterpriseAnnually : EnterprisePlan
{
public EnterpriseAnnually() : base(true)
{
}
}
}

View File

@@ -1,492 +0,0 @@
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Tax.Services.Implementations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Tax.Services;
[SutProviderCustomize]
public class BusinessUseAutomaticTaxStrategyTests
{
[Theory]
[BitAutoData]
public void GetUpdateOptions_ReturnsNull_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(false);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.Null(actual);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_ReturnsNull_WhenSubscriptionDoesNotNeedUpdating(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "US",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.Null(actual);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.False(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForAmericanCustomers(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = "US",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.True(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>
{
new()
{
Country = "ES",
Type = "eu_vat",
Value = "ESZ8880999Z"
}
}
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.True(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_ThrowsArgumentNullException_WhenTaxIdsIsNull(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = null
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
Assert.Throws<ArgumentNullException>(() => sutProvider.Sut.GetUpdateOptions(subscription));
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>()
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.False(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsNothing_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
Customer = new Customer
{
Address = new()
{
Country = "US"
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(false);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.Null(options.AutomaticTax);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsNothing_WhenSubscriptionDoesNotNeedUpdating(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "US",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.Null(options.AutomaticTax);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.False(options.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForAmericanCustomers(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = "US",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.True(options.AutomaticTax!.Enabled);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>
{
new()
{
Country = "ES",
Type = "eu_vat",
Value = "ESZ8880999Z"
}
}
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.True(options.AutomaticTax!.Enabled);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_ThrowsArgumentNullException_WhenTaxIdsIsNull(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = null
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
Assert.Throws<ArgumentNullException>(() => sutProvider.Sut.SetUpdateOptions(options, subscription));
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>()
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.False(options.AutomaticTax!.Enabled);
}
}

View File

@@ -1,35 +0,0 @@
using Bit.Core.Billing.Tax.Services;
using Stripe;
namespace Bit.Core.Test.Billing.Tax.Services;
/// <param name="isAutomaticTaxEnabled">
/// Whether the subscription options will have automatic tax enabled or not.
/// </param>
public class FakeAutomaticTaxStrategy(
bool isAutomaticTaxEnabled) : IAutomaticTaxStrategy
{
public SubscriptionUpdateOptions? GetUpdateOptions(Subscription subscription)
{
return new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled }
};
}
public void SetCreateOptions(SubscriptionCreateOptions options, Customer customer)
{
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled };
}
public void SetUpdateOptions(SubscriptionUpdateOptions options, Subscription subscription)
{
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled };
}
public void SetInvoiceCreatePreviewOptions(InvoiceCreatePreviewOptions options)
{
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled };
}
}

View File

@@ -1,217 +0,0 @@
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Tax.Services.Implementations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Tax.Services;
[SutProviderCustomize]
public class PersonalUseAutomaticTaxStrategyTests
{
[Theory]
[BitAutoData]
public void GetUpdateOptions_ReturnsNull_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled(
SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(false);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.Null(actual);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_ReturnsNull_WhenSubscriptionDoesNotNeedUpdating(
SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "US",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.Null(actual);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid(
SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.False(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData("CA")]
[BitAutoData("ES")]
[BitAutoData("US")]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForAllCountries(
string country, SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = country
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.True(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData("CA")]
[BitAutoData("ES")]
[BitAutoData("US")]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds(
string country, SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = country,
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>
{
new()
{
Country = "ES",
Type = "eu_vat",
Value = "ESZ8880999Z"
}
}
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.True(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData("CA")]
[BitAutoData("ES")]
[BitAutoData("US")]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds(
string country, SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = country
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>()
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.True(actual.AutomaticTax.Enabled);
}
}

View File

@@ -0,0 +1,733 @@
using System.Security.Claims;
using Bit.Core.AdminConsole.Context;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Identity;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Context;
[SutProviderCustomize]
public class CurrentContextTests
{
#region BuildAsync(HttpContext) Tests
[Theory, BitAutoData]
public async Task BuildAsync_HttpContext_SetsHttpContext(
SutProvider<CurrentContext> sutProvider)
{
var httpContext = new DefaultHttpContext();
var globalSettings = new Core.Settings.GlobalSettings();
// Act
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
// Assert
Assert.Equal(httpContext, sutProvider.Sut.HttpContext);
}
[Theory, BitAutoData]
public async Task BuildAsync_HttpContext_OnlyBuildsOnce(
SutProvider<CurrentContext> sutProvider)
{
var httpContext = new DefaultHttpContext();
var globalSettings = new Core.Settings.GlobalSettings();
// Arrange
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
var firstContext = sutProvider.Sut.HttpContext;
var secondHttpContext = new DefaultHttpContext();
// Act
await sutProvider.Sut.BuildAsync(secondHttpContext, globalSettings);
// Assert
Assert.Equal(firstContext, sutProvider.Sut.HttpContext);
Assert.NotEqual(secondHttpContext, sutProvider.Sut.HttpContext);
}
[Theory, BitAutoData]
public async Task BuildAsync_HttpContext_SetsDeviceIdentifier(
SutProvider<CurrentContext> sutProvider,
string expectedValue)
{
var httpContext = new DefaultHttpContext();
var globalSettings = new Core.Settings.GlobalSettings();
sutProvider.Sut.DeviceIdentifier = null;
// Arrange
httpContext.Request.Headers["Device-Identifier"] = expectedValue;
// Act
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
// Assert
Assert.Equal(expectedValue, sutProvider.Sut.DeviceIdentifier);
}
[Theory, BitAutoData]
public async Task BuildAsync_HttpContext_SetsCountryName(
SutProvider<CurrentContext> sutProvider,
string countryName)
{
var httpContext = new DefaultHttpContext();
var globalSettings = new Core.Settings.GlobalSettings();
// Arrange
httpContext.Request.Headers["country-name"] = countryName;
// Act
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
// Assert
Assert.Equal(countryName, sutProvider.Sut.CountryName);
}
[Theory, BitAutoData]
public async Task BuildAsync_HttpContext_SetsDeviceType(
SutProvider<CurrentContext> sutProvider)
{
var httpContext = new DefaultHttpContext();
var globalSettings = new Core.Settings.GlobalSettings();
// Arrange
var deviceType = DeviceType.Android;
httpContext.Request.Headers["Device-Type"] = ((int)deviceType).ToString();
// Act
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
// Assert
Assert.Equal(deviceType, sutProvider.Sut.DeviceType);
}
[Theory, BitAutoData]
public async Task BuildAsync_HttpContext_SetsCloudflareFlags(
SutProvider<CurrentContext> sutProvider)
{
var httpContext = new DefaultHttpContext();
var globalSettings = new Core.Settings.GlobalSettings();
sutProvider.Sut.BotScore = null;
// Arrange
var botScore = 85;
httpContext.Request.Headers["X-Cf-Bot-Score"] = botScore.ToString();
httpContext.Request.Headers["X-Cf-Worked-Proxied"] = "1";
httpContext.Request.Headers["X-Cf-Is-Bot"] = "1";
httpContext.Request.Headers["X-Cf-Maybe-Bot"] = "1";
// Act
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
// Assert
Assert.True(sutProvider.Sut.CloudflareWorkerProxied);
Assert.True(sutProvider.Sut.IsBot);
Assert.True(sutProvider.Sut.MaybeBot);
Assert.Equal(botScore, sutProvider.Sut.BotScore);
}
[Theory, BitAutoData]
public async Task BuildAsync_HttpContext_SetsClientVersion(
SutProvider<CurrentContext> sutProvider)
{
var httpContext = new DefaultHttpContext();
var globalSettings = new Core.Settings.GlobalSettings();
// Arrange
var version = "2024.1.0";
httpContext.Request.Headers["Bitwarden-Client-Version"] = version;
httpContext.Request.Headers["Is-Prerelease"] = "1";
// Act
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
// Assert
Assert.Equal(new Version(version), sutProvider.Sut.ClientVersion);
Assert.True(sutProvider.Sut.ClientVersionIsPrerelease);
}
#endregion
#region SetContextAsync Tests
[Theory, BitAutoData]
public async Task SetContextAsync_NullUser_DoesNotThrow(
SutProvider<CurrentContext> sutProvider)
{
// Act & Assert
await sutProvider.Sut.SetContextAsync(null);
// Should not throw
}
[Theory, BitAutoData]
public async Task SetContextAsync_UserWithNoClaims_DoesNotThrow(
SutProvider<CurrentContext> sutProvider)
{
// Arrange
var user = new ClaimsPrincipal();
// Act & Assert
await sutProvider.Sut.SetContextAsync(user);
// Should not throw
}
[Theory, BitAutoData]
public async Task SetContextAsync_SendClient_ShortCircuits(
SutProvider<CurrentContext> sutProvider,
Guid userId)
{
// Arrange
sutProvider.Sut.UserId = null;
var claims = new List<Claim>
{
new(Claims.Type, IdentityClientType.Send.ToString()),
new("sub", userId.ToString())
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Equal(IdentityClientType.Send, sutProvider.Sut.IdentityClientType);
Assert.Null(sutProvider.Sut.UserId); // Should not be set for Send clients
}
[Theory, BitAutoData]
public async Task SetContextAsync_RegularUser_SetsUserId(
SutProvider<CurrentContext> sutProvider,
Guid userId,
string clientId)
{
// Arrange
var claims = new List<Claim>
{
new("sub", userId.ToString()),
new("client_id", clientId)
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Equal(userId, sutProvider.Sut.UserId);
Assert.Equal(clientId, sutProvider.Sut.ClientId);
}
[Theory, BitAutoData]
public async Task SetContextAsync_InstallationClient_SetsInstallationId(
SutProvider<CurrentContext> sutProvider,
Guid installationId)
{
// Arrange
var claims = new List<Claim>
{
new("client_id", "installation.12345"),
new("client_sub", installationId.ToString())
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Equal(installationId, sutProvider.Sut.InstallationId);
}
[Theory, BitAutoData]
public async Task SetContextAsync_OrganizationClient_SetsOrganizationId(
SutProvider<CurrentContext> sutProvider,
Guid organizationId)
{
// Arrange
var claims = new List<Claim>
{
new("client_id", "organization.12345"),
new("client_sub", organizationId.ToString())
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Equal(organizationId, sutProvider.Sut.OrganizationId);
}
[Theory, BitAutoData]
public async Task SetContextAsync_ServiceAccount_SetsServiceAccountOrganizationId(
SutProvider<CurrentContext> sutProvider,
Guid organizationId)
{
// Arrange
var claims = new List<Claim>
{
new(Claims.Type, IdentityClientType.ServiceAccount.ToString()),
new(Claims.Organization, organizationId.ToString())
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Equal(IdentityClientType.ServiceAccount, sutProvider.Sut.IdentityClientType);
Assert.Equal(organizationId, sutProvider.Sut.ServiceAccountOrganizationId);
}
[Theory, BitAutoData]
public async Task SetContextAsync_WithDeviceClaims_SetsDeviceInfo(
SutProvider<CurrentContext> sutProvider,
string deviceIdentifier)
{
// Arrange
var claims = new List<Claim>
{
new(Claims.Device, deviceIdentifier),
new(Claims.DeviceType, ((int)DeviceType.iOS).ToString())
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Equal(deviceIdentifier, sutProvider.Sut.DeviceIdentifier);
Assert.Equal(DeviceType.iOS, sutProvider.Sut.DeviceType);
}
#endregion
#region Organization Claims Tests
[Theory]
[BitAutoData(Claims.OrganizationOwner, OrganizationUserType.Owner)]
[BitAutoData(Claims.OrganizationAdmin, OrganizationUserType.Admin)]
[BitAutoData(Claims.OrganizationUser, OrganizationUserType.User)]
public async Task SetContextAsync_OrganizationClaims_SetsOrganizations(
string userOrgAssociation,
OrganizationUserType userType,
SutProvider<CurrentContext> sutProvider,
Guid org1Id,
Guid org2Id)
{
// Arrange
var claims = new List<Claim>
{
new(userOrgAssociation, org1Id.ToString()),
new(userOrgAssociation, org2Id.ToString()),
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Equal(2, sutProvider.Sut.Organizations.Count);
Assert.All(sutProvider.Sut.Organizations, org => Assert.Equal(userType, org.Type));
Assert.Contains(sutProvider.Sut.Organizations, org => org.Id == org1Id);
Assert.Contains(sutProvider.Sut.Organizations, org => org.Id == org2Id);
}
[Theory, BitAutoData]
public async Task SetContextAsync_OrganizationCustomClaims_SetsOrganizationsWithPermissions(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
var claims = new List<Claim>
{
new(Claims.OrganizationCustom, orgId.ToString()),
new("accesseventlogs", orgId.ToString()),
new("manageusers", orgId.ToString())
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Single(sutProvider.Sut.Organizations);
var org = sutProvider.Sut.Organizations.First();
Assert.Equal(OrganizationUserType.Custom, org.Type);
Assert.Equal(orgId, org.Id);
Assert.True(org.Permissions.AccessEventLogs);
Assert.True(org.Permissions.ManageUsers);
Assert.False(org.Permissions.ManageGroups);
}
[Theory, BitAutoData]
public async Task SetContextAsync_SecretsManagerAccess_SetsAccessSecretsManager(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
var claims = new List<Claim>
{
new(Claims.OrganizationOwner, orgId.ToString()),
new(Claims.SecretsManagerAccess, orgId.ToString())
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Single(sutProvider.Sut.Organizations);
Assert.True(sutProvider.Sut.Organizations.First().AccessSecretsManager);
}
#endregion
#region Provider Claims Tests
[Theory, BitAutoData]
public async Task SetContextAsync_ProviderAdminClaims_SetsProviders(
SutProvider<CurrentContext> sutProvider,
Guid providerId)
{
// Arrange
var claims = new List<Claim>
{
new(Claims.ProviderAdmin, providerId.ToString())
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Single(sutProvider.Sut.Providers);
Assert.Equal(ProviderUserType.ProviderAdmin, sutProvider.Sut.Providers.First().Type);
Assert.Equal(providerId, sutProvider.Sut.Providers.First().Id);
}
[Theory, BitAutoData]
public async Task SetContextAsync_ProviderServiceUserClaims_SetsProviders(
SutProvider<CurrentContext> sutProvider,
Guid providerId)
{
// Arrange
var claims = new List<Claim>
{
new(Claims.ProviderServiceUser, providerId.ToString())
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
// Act
await sutProvider.Sut.SetContextAsync(user);
// Assert
Assert.Single(sutProvider.Sut.Providers);
Assert.Equal(ProviderUserType.ServiceUser, sutProvider.Sut.Providers.First().Type);
Assert.Equal(providerId, sutProvider.Sut.Providers.First().Id);
}
#endregion
#region Organization Permission Tests
[Theory, BitAutoData]
public async Task OrganizationUser_WithDirectAccess_ReturnsTrue(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
{
new() { Id = orgId, Type = OrganizationUserType.User }
};
// Act
var result = await sutProvider.Sut.OrganizationUser(orgId);
// Assert
Assert.True(result);
}
[Theory, BitAutoData]
public async Task OrganizationUser_WithoutAccess_ReturnsFalse(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>();
// Act
var result = await sutProvider.Sut.OrganizationUser(orgId);
// Assert
Assert.False(result);
}
[Theory, BitAutoData]
public async Task OrganizationAdmin_WithAdminAccess_ReturnsTrue(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
{
new() { Id = orgId, Type = OrganizationUserType.Admin }
};
// Act
var result = await sutProvider.Sut.OrganizationAdmin(orgId);
// Assert
Assert.True(result);
}
[Theory, BitAutoData]
public async Task OrganizationOwner_WithOwnerAccess_ReturnsTrue(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
{
new() { Id = orgId, Type = OrganizationUserType.Owner }
};
// Act
var result = await sutProvider.Sut.OrganizationOwner(orgId);
// Assert
Assert.True(result);
}
[Theory, BitAutoData]
public async Task OrganizationCustom_WithCustomAccess_ReturnsTrue(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
{
new() { Id = orgId, Type = OrganizationUserType.Custom }
};
// Act
var result = await sutProvider.Sut.OrganizationCustom(orgId);
// Assert
Assert.True(result);
}
[Theory, BitAutoData]
public async Task AccessEventLogs_WithPermission_ReturnsTrue(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
{
new()
{
Id = orgId,
Type = OrganizationUserType.Custom,
Permissions = new Permissions { AccessEventLogs = true }
}
};
// Act
var result = await sutProvider.Sut.AccessEventLogs(orgId);
// Assert
Assert.True(result);
}
#endregion
#region Provider Permission Tests
[Theory, BitAutoData]
public void ProviderProviderAdmin_WithAdminAccess_ReturnsTrue(
SutProvider<CurrentContext> sutProvider,
Guid providerId)
{
// Arrange
sutProvider.Sut.Providers = new List<CurrentContextProvider>
{
new() { Id = providerId, Type = ProviderUserType.ProviderAdmin }
};
// Act
var result = sutProvider.Sut.ProviderProviderAdmin(providerId);
// Assert
Assert.True(result);
}
[Theory, BitAutoData]
public void ProviderUser_WithAnyAccess_ReturnsTrue(
SutProvider<CurrentContext> sutProvider,
Guid providerId)
{
// Arrange
sutProvider.Sut.Providers = new List<CurrentContextProvider>
{
new() { Id = providerId, Type = ProviderUserType.ServiceUser }
};
// Act
var result = sutProvider.Sut.ProviderUser(providerId);
// Assert
Assert.True(result);
}
#endregion
#region Secrets Manager Tests
[Theory, BitAutoData]
public void AccessSecretsManager_WithServiceAccount_ReturnsTrue(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
sutProvider.Sut.ServiceAccountOrganizationId = orgId;
// Act
var result = sutProvider.Sut.AccessSecretsManager(orgId);
// Assert
Assert.True(result);
}
[Theory, BitAutoData]
public void AccessSecretsManager_WithOrgAccess_ReturnsTrue(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
{
new() { Id = orgId, AccessSecretsManager = true }
};
// Act
var result = sutProvider.Sut.AccessSecretsManager(orgId);
// Assert
Assert.True(result);
}
[Theory, BitAutoData]
public void AccessSecretsManager_WithoutAccess_ReturnsFalse(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
{
new() { Id = orgId, AccessSecretsManager = false }
};
// Act
var result = sutProvider.Sut.AccessSecretsManager(orgId);
// Assert
Assert.False(result);
}
#endregion
#region Membership Loading Tests
[Theory, BitAutoData]
public async Task OrganizationMembershipAsync_LoadsFromRepository(
SutProvider<CurrentContext> sutProvider,
Guid userId,
List<OrganizationUserOrganizationDetails> userOrgs)
{
// Arrange
sutProvider.Sut.UserId = userId;
sutProvider.Sut.Organizations = null;
var organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
userOrgs.ForEach(org => org.Status = OrganizationUserStatusType.Confirmed);
// Test complains about the JSON object that we store permissions as, so just set to empty object to pass the test.
userOrgs.ForEach(org => org.Permissions = "{}");
organizationUserRepository.GetManyDetailsByUserAsync(userId)
.Returns(userOrgs);
// Act
var result = await sutProvider.Sut.OrganizationMembershipAsync(organizationUserRepository, userId);
// Assert
Assert.Equal(userOrgs.Count, result.Count);
Assert.Equal(userId, sutProvider.Sut.UserId);
await organizationUserRepository.Received(1).GetManyDetailsByUserAsync(userId);
}
[Theory, BitAutoData]
public async Task ProviderMembershipAsync_LoadsFromRepository(
SutProvider<CurrentContext> sutProvider,
Guid userId,
List<ProviderUser> userProviders)
{
// Arrange
sutProvider.Sut.UserId = userId;
sutProvider.Sut.Providers = null;
var providerUserRepository = Substitute.For<IProviderUserRepository>();
userProviders.ForEach(provider => provider.Status = ProviderUserStatusType.Confirmed);
// Test complains about the JSON object that we store permissions as, so just set to empty object to pass the test.
userProviders.ForEach(provider => provider.Permissions = "{}");
providerUserRepository.GetManyByUserAsync(userId)
.Returns(userProviders);
// Act
var result = await sutProvider.Sut.ProviderMembershipAsync(providerUserRepository, userId);
// Assert
Assert.Equal(userProviders.Count, result.Count);
Assert.Equal(userId, sutProvider.Sut.UserId);
await providerUserRepository.Received(1).GetManyByUserAsync(userId);
}
#endregion
#region Utility Tests
[Theory, BitAutoData]
public void GetOrganization_WithExistingOrg_ReturnsOrganization(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
var org = new CurrentContextOrganization { Id = orgId };
sutProvider.Sut.Organizations = new List<CurrentContextOrganization> { org };
// Act
var result = sutProvider.Sut.GetOrganization(orgId);
// Assert
Assert.Equal(org, result);
}
[Theory, BitAutoData]
public void GetOrganization_WithNonExistingOrg_ReturnsNull(
SutProvider<CurrentContext> sutProvider,
Guid orgId)
{
// Arrange
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>();
// Act
var result = sutProvider.Sut.GetOrganization(orgId);
// Assert
Assert.Null(result);
}
#endregion
}

View File

@@ -1,194 +0,0 @@
using AutoFixture;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class DeleteOrganizationReportCommandTests
{
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withValidRequest_Success(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var OrganizationReports = fixture.CreateMany<OrganizationReport>(2).ToList();
// only take one id from the list - we only want to drop one record
var request = fixture.Build<DropOrganizationReportRequest>()
.With(x => x.OrganizationReportIds,
OrganizationReports.Select(x => x.Id).Take(1).ToList())
.Create();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(OrganizationReports);
// Act
await sutProvider.Sut.DropOrganizationReportAsync(request);
// Assert
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1)
.DeleteAsync(Arg.Is<OrganizationReport>(_ =>
request.OrganizationReportIds.Contains(_.Id)));
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withValidRequest_nothingToDrop(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var OrganizationReports = fixture.CreateMany<OrganizationReport>(2).ToList();
// we are passing invalid data
var request = fixture.Build<DropOrganizationReportRequest>()
.With(x => x.OrganizationReportIds, new List<Guid> { Guid.NewGuid() })
.Create();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(OrganizationReports);
// Act
await sutProvider.Sut.DropOrganizationReportAsync(request);
// Assert
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(0)
.DeleteAsync(Arg.Any<OrganizationReport>());
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withNodata_fails(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
// we are passing invalid data
var request = fixture.Build<DropOrganizationReportRequest>()
.Create();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(null as List<OrganizationReport>);
// Act
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DropOrganizationReportAsync(request));
// Assert
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(0)
.DeleteAsync(Arg.Any<OrganizationReport>());
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withInvalidOrganizationId_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<DropOrganizationReportRequest>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(null as List<OrganizationReport>);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withInvalidOrganizationReportId_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<DropOrganizationReportRequest>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(new List<OrganizationReport>());
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withNullOrganizationId_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<DropOrganizationReportRequest>()
.With(x => x.OrganizationId, default(Guid))
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withNullOrganizationReportIds_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<DropOrganizationReportRequest>()
.With(x => x.OrganizationReportIds, default(List<Guid>))
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withEmptyOrganizationReportIds_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<DropOrganizationReportRequest>()
.With(x => x.OrganizationReportIds, new List<Guid>())
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withEmptyRequest_ShouldThrowError(
SutProvider<DropOrganizationReportCommand> sutProvider)
{
// Arrange
var request = new DropOrganizationReportRequest();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
Assert.Equal("No data found.", exception.Message);
}
}

View File

@@ -0,0 +1,116 @@
using AutoFixture;
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class GetOrganizationReportApplicationDataQueryTests
{
[Theory]
[BitAutoData]
public async Task GetOrganizationReportApplicationDataAsync_WithValidParams_ShouldReturnApplicationData(
SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
var reportId = fixture.Create<Guid>();
var applicationDataResponse = fixture.Build<OrganizationReportApplicationDataResponse>()
.Create();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetApplicationDataAsync(reportId)
.Returns(applicationDataResponse);
// Act
var result = await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId);
// Assert
Assert.NotNull(result);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1).GetApplicationDataAsync(reportId);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportApplicationDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)
{
// Arrange
var reportId = Guid.NewGuid();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(Guid.Empty, reportId));
Assert.Equal("OrganizationId is required.", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().GetApplicationDataAsync(Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportApplicationDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(
SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, Guid.Empty));
Assert.Equal("ReportId is required.", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().GetApplicationDataAsync(Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportApplicationDataAsync_WhenDataNotFound_ShouldThrowNotFoundException(
SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
var reportId = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetApplicationDataAsync(reportId)
.Returns((OrganizationReportApplicationDataResponse)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId));
Assert.Equal("Organization report application data not found.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportApplicationDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(
SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
var reportId = Guid.NewGuid();
var expectedMessage = "Database connection failed";
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetApplicationDataAsync(reportId)
.Throws(new InvalidOperationException(expectedMessage));
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId));
Assert.Equal(expectedMessage, exception.Message);
}
}

View File

@@ -0,0 +1,116 @@
using AutoFixture;
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class GetOrganizationReportDataQueryTests
{
[Theory]
[BitAutoData]
public async Task GetOrganizationReportDataAsync_WithValidParams_ShouldReturnReportData(
SutProvider<GetOrganizationReportDataQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
var reportId = fixture.Create<Guid>();
var reportDataResponse = fixture.Build<OrganizationReportDataResponse>()
.Create();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetReportDataAsync(reportId)
.Returns(reportDataResponse);
// Act
var result = await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId);
// Assert
Assert.NotNull(result);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1).GetReportDataAsync(reportId);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
SutProvider<GetOrganizationReportDataQuery> sutProvider)
{
// Arrange
var reportId = Guid.NewGuid();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetOrganizationReportDataAsync(Guid.Empty, reportId));
Assert.Equal("OrganizationId is required.", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().GetReportDataAsync(Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(
SutProvider<GetOrganizationReportDataQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, Guid.Empty));
Assert.Equal("ReportId is required.", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().GetReportDataAsync(Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportDataAsync_WhenDataNotFound_ShouldThrowNotFoundException(
SutProvider<GetOrganizationReportDataQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
var reportId = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetReportDataAsync(reportId)
.Returns((OrganizationReportDataResponse)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId));
Assert.Equal("Organization report data not found.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(
SutProvider<GetOrganizationReportDataQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
var reportId = Guid.NewGuid();
var expectedMessage = "Database connection failed";
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetReportDataAsync(reportId)
.Throws(new InvalidOperationException(expectedMessage));
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId));
Assert.Equal(expectedMessage, exception.Message);
}
}

View File

@@ -1,188 +0,0 @@
using AutoFixture;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class GetOrganizationReportQueryTests
{
[Theory]
[BitAutoData]
public async Task GetOrganizationReportAsync_WithValidOrganizationId_ShouldReturnOrganizationReport(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(fixture.CreateMany<OrganizationReport>(2).ToList());
// Act
var result = await sutProvider.Sut.GetOrganizationReportAsync(organizationId);
// Assert
Assert.NotNull(result);
Assert.True(result.Count() == 2);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportAsync_WithInvalidOrganizationId_ShouldFail(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Is<Guid>(x => x == Guid.Empty))
.Returns(new List<OrganizationReport>());
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(Guid.Empty));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetLatestOrganizationReportAsync_WithValidOrganizationId_ShouldReturnOrganizationReport(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetLatestByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(fixture.Create<OrganizationReport>());
// Act
var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId);
// Assert
Assert.NotNull(result);
}
[Theory]
[BitAutoData]
public async Task GetLatestOrganizationReportAsync_WithInvalidOrganizationId_ShouldFail(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetLatestByOrganizationIdAsync(Arg.Is<Guid>(x => x == Guid.Empty))
.Returns(default(OrganizationReport));
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(Guid.Empty));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportAsync_WithNoReports_ShouldReturnEmptyList(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(new List<OrganizationReport>());
// Act
var result = await sutProvider.Sut.GetOrganizationReportAsync(organizationId);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Theory]
[BitAutoData]
public async Task GetLatestOrganizationReportAsync_WithNoReports_ShouldReturnNull(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetLatestByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(default(OrganizationReport));
// Act
var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId);
// Assert
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportAsync_WithNullOrganizationId_ShouldThrowException(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = default(Guid);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(organizationId));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetLatestOrganizationReportAsync_WithNullOrganizationId_ShouldThrowException(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = default(Guid);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowException(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = Guid.Empty;
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(organizationId));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetLatestOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowException(
SutProvider<GetOrganizationReportQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = Guid.Empty;
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId));
// Assert
Assert.Equal("OrganizationId is required.", exception.Message);
}
}

View File

@@ -0,0 +1,133 @@
using AutoFixture;
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class GetOrganizationReportSummaryDataByDateRangeQueryTests
{
[Theory]
[BitAutoData]
public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithValidParams_ShouldReturnSummaryData(
SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
var reportId = fixture.Create<Guid>();
var startDate = DateTime.UtcNow.AddDays(-30);
var endDate = DateTime.UtcNow;
var summaryDataList = fixture.Build<OrganizationReportSummaryDataResponse>()
.CreateMany(3);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate)
.Returns(summaryDataList);
// Act
var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);
// Assert
Assert.NotNull(result);
Assert.Equal(3, result.Count());
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1).GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)
{
// Arrange
var reportId = Guid.NewGuid();
var startDate = DateTime.UtcNow.AddDays(-30);
var endDate = DateTime.UtcNow;
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(Guid.Empty, startDate, endDate));
Assert.Equal("OrganizationId is required", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive()
.GetSummaryDataByDateRangeAsync(
Arg.Any<Guid>(),
Arg.Any<DateTime>(),
Arg.Any<DateTime>());
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithStartDateAfterEndDate_ShouldThrowBadRequestException(
SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
var reportId = Guid.NewGuid();
var startDate = DateTime.UtcNow;
var endDate = DateTime.UtcNow.AddDays(-30);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate));
Assert.Equal("StartDate must be earlier than or equal to EndDate", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().GetSummaryDataByDateRangeAsync(Arg.Any<Guid>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithEmptyResult_ShouldReturnEmptyList(
SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
var reportId = Guid.NewGuid();
var startDate = DateTime.UtcNow.AddDays(-30);
var endDate = DateTime.UtcNow;
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate)
.Returns(new List<OrganizationReportSummaryDataResponse>());
// Act
var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WhenRepositoryThrowsException_ShouldPropagateException(
SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
var reportId = Guid.NewGuid();
var startDate = DateTime.UtcNow.AddDays(-30);
var endDate = DateTime.UtcNow;
var expectedMessage = "Database connection failed";
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate)
.Throws(new InvalidOperationException(expectedMessage));
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate));
Assert.Equal(expectedMessage, exception.Message);
}
}

View File

@@ -0,0 +1,116 @@
using AutoFixture;
using Bit.Core.Dirt.Models.Data;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class GetOrganizationReportSummaryDataQueryTests
{
[Theory]
[BitAutoData]
public async Task GetOrganizationReportSummaryDataAsync_WithValidParams_ShouldReturnSummaryData(
SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)
{
// Arrange
var fixture = new Fixture();
var organizationId = fixture.Create<Guid>();
var reportId = fixture.Create<Guid>();
var summaryDataResponse = fixture.Build<OrganizationReportSummaryDataResponse>()
.Create();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetSummaryDataAsync(reportId)
.Returns(summaryDataResponse);
// Act
var result = await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId);
// Assert
Assert.NotNull(result);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1).GetSummaryDataAsync(reportId);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportSummaryDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)
{
// Arrange
var reportId = Guid.NewGuid();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(Guid.Empty, reportId));
Assert.Equal("OrganizationId is required.", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().GetSummaryDataAsync(Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportSummaryDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(
SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, Guid.Empty));
Assert.Equal("ReportId is required.", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().GetSummaryDataAsync(Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportSummaryDataAsync_WhenDataNotFound_ShouldThrowNotFoundException(
SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
var reportId = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetSummaryDataAsync(reportId)
.Returns((OrganizationReportSummaryDataResponse)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId));
Assert.Equal("Organization report summary data not found.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationReportSummaryDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(
SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)
{
// Arrange
var organizationId = Guid.NewGuid();
var reportId = Guid.NewGuid();
var expectedMessage = "Database connection failed";
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetSummaryDataAsync(reportId)
.Throws(new InvalidOperationException(expectedMessage));
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId));
Assert.Equal(expectedMessage, exception.Message);
}
}

View File

@@ -0,0 +1,252 @@
using AutoFixture;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class UpdateOrganizationReportApplicationDataCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportApplicationDataAsync_WithValidRequest_ShouldReturnUpdatedReport(
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()
.With(x => x.Id, Guid.NewGuid())
.With(x => x.OrganizationId, Guid.NewGuid())
.With(x => x.ApplicationData, "updated application data")
.Create();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.Id)
.With(x => x.OrganizationId, request.OrganizationId)
.Create();
var updatedReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.Id)
.With(x => x.OrganizationId, request.OrganizationId)
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.Id)
.Returns(existingReport);
sutProvider.GetDependency<IOrganizationReportRepository>()
.UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData)
.Returns(updatedReport);
// Act
var result = await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal(updatedReport.Id, result.Id);
Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1).UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()
.With(x => x.OrganizationId, Guid.Empty)
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
Assert.Equal("OrganizationId is required", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().UpdateApplicationDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()
.With(x => x.Id, Guid.Empty)
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
Assert.Equal("Id is required", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().UpdateApplicationDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportApplicationDataAsync_WithInvalidOrganization_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns((Organization)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
Assert.Equal("Invalid Organization", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyApplicationData_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()
.With(x => x.ApplicationData, string.Empty)
.Create();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
Assert.Equal("Application Data is required", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportApplicationDataAsync_WithNullApplicationData_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()
.With(x => x.ApplicationData, (string)null)
.Create();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
Assert.Equal("Application Data is required", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportApplicationDataAsync_WithNonExistentReport_ShouldThrowNotFoundException(
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.Id)
.Returns((OrganizationReport)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
Assert.Equal("Organization report not found", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportApplicationDataAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.Id)
.With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.Id)
.Returns(existingReport);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
Assert.Equal("Organization report does not belong to the specified organization", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportApplicationDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.Id)
.With(x => x.OrganizationId, request.OrganizationId)
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.Id)
.Returns(existingReport);
sutProvider.GetDependency<IOrganizationReportRepository>()
.UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData)
.Throws(new InvalidOperationException("Database connection failed"));
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
Assert.Equal("Database connection failed", exception.Message);
}
}

View File

@@ -0,0 +1,230 @@
using AutoFixture;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class UpdateOrganizationReportCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportAsync_WithValidRequest_ShouldReturnUpdatedReport(
SutProvider<UpdateOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportRequest>()
.With(x => x.ReportId, Guid.NewGuid())
.With(x => x.OrganizationId, Guid.NewGuid())
.With(x => x.ReportData, "updated report data")
.Create();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, request.OrganizationId)
.Create();
var updatedReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, request.OrganizationId)
.With(x => x.ReportData, request.ReportData)
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns(existingReport);
sutProvider.GetDependency<IOrganizationReportRepository>()
.UpsertAsync(Arg.Any<OrganizationReport>())
.Returns(Task.CompletedTask);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns(existingReport, updatedReport);
// Act
var result = await sutProvider.Sut.UpdateOrganizationReportAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal(updatedReport.Id, result.Id);
Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);
Assert.Equal(updatedReport.ReportData, result.ReportData);
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1).GetByIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(2).GetByIdAsync(request.ReportId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1).UpsertAsync(Arg.Any<OrganizationReport>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportRequest>()
.With(x => x.OrganizationId, Guid.Empty)
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
Assert.Equal("OrganizationId is required", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().UpsertAsync(Arg.Any<OrganizationReport>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportAsync_WithEmptyReportId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportRequest>()
.With(x => x.ReportId, Guid.Empty)
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
Assert.Equal("ReportId is required", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().UpsertAsync(Arg.Any<OrganizationReport>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportAsync_WithInvalidOrganization_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportRequest>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns((Organization)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
Assert.Equal("Invalid Organization", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportAsync_WithEmptyReportData_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportRequest>()
.With(x => x.ReportData, string.Empty)
.Create();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
Assert.Equal("Report Data is required", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportAsync_WithNullReportData_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportRequest>()
.With(x => x.ReportData, (string)null)
.Create();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
Assert.Equal("Report Data is required", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportAsync_WithNonExistentReport_ShouldThrowNotFoundException(
SutProvider<UpdateOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportRequest>();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns((OrganizationReport)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
Assert.Equal("Organization report not found", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportRequest>();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns(existingReport);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
Assert.Equal("Organization report does not belong to the specified organization", exception.Message);
}
}

View File

@@ -0,0 +1,252 @@
using AutoFixture;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class UpdateOrganizationReportDataCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportDataAsync_WithValidRequest_ShouldReturnUpdatedReport(
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportDataRequest>()
.With(x => x.ReportId, Guid.NewGuid())
.With(x => x.OrganizationId, Guid.NewGuid())
.With(x => x.ReportData, "updated report data")
.Create();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, request.OrganizationId)
.Create();
var updatedReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, request.OrganizationId)
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns(existingReport);
sutProvider.GetDependency<IOrganizationReportRepository>()
.UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData)
.Returns(updatedReport);
// Act
var result = await sutProvider.Sut.UpdateOrganizationReportDataAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal(updatedReport.Id, result.Id);
Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1).UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportDataRequest>()
.With(x => x.OrganizationId, Guid.Empty)
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
Assert.Equal("OrganizationId is required", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().UpdateReportDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportDataRequest>()
.With(x => x.ReportId, Guid.Empty)
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
Assert.Equal("ReportId is required", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().UpdateReportDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportDataAsync_WithInvalidOrganization_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportDataRequest>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns((Organization)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
Assert.Equal("Invalid Organization", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportDataAsync_WithEmptyReportData_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportDataRequest>()
.With(x => x.ReportData, string.Empty)
.Create();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
Assert.Equal("Report Data is required", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportDataAsync_WithNullReportData_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportDataRequest>()
.With(x => x.ReportData, (string)null)
.Create();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
Assert.Equal("Report Data is required", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportDataAsync_WithNonExistentReport_ShouldThrowNotFoundException(
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportDataRequest>();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns((OrganizationReport)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
Assert.Equal("Organization report not found", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportDataAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportDataRequest>();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns(existingReport);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
Assert.Equal("Organization report does not belong to the specified organization", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportDataRequest>();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, request.OrganizationId)
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns(existingReport);
sutProvider.GetDependency<IOrganizationReportRepository>()
.UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData)
.Throws(new InvalidOperationException("Database connection failed"));
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
Assert.Equal("Database connection failed", exception.Message);
}
}

View File

@@ -0,0 +1,252 @@
using AutoFixture;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Dirt.ReportFeatures;
[SutProviderCustomize]
public class UpdateOrganizationReportSummaryCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportSummaryAsync_WithValidRequest_ShouldReturnUpdatedReport(
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()
.With(x => x.ReportId, Guid.NewGuid())
.With(x => x.OrganizationId, Guid.NewGuid())
.With(x => x.SummaryData, "updated summary data")
.Create();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, request.OrganizationId)
.Create();
var updatedReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, request.OrganizationId)
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns(existingReport);
sutProvider.GetDependency<IOrganizationReportRepository>()
.UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData)
.Returns(updatedReport);
// Act
var result = await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal(updatedReport.Id, result.Id);
Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.Received(1).UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportSummaryAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()
.With(x => x.OrganizationId, Guid.Empty)
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
Assert.Equal("OrganizationId is required", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().UpdateSummaryDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportSummaryAsync_WithEmptyReportId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()
.With(x => x.ReportId, Guid.Empty)
.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
Assert.Equal("ReportId is required", exception.Message);
await sutProvider.GetDependency<IOrganizationReportRepository>()
.DidNotReceive().UpdateSummaryDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportSummaryAsync_WithInvalidOrganization_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns((Organization)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
Assert.Equal("Invalid Organization", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportSummaryAsync_WithEmptySummaryData_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()
.With(x => x.SummaryData, string.Empty)
.Create();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
Assert.Equal("Summary Data is required", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportSummaryAsync_WithNullSummaryData_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()
.With(x => x.SummaryData, (string)null)
.Create();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
Assert.Equal("Summary Data is required", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportSummaryAsync_WithNonExistentReport_ShouldThrowNotFoundException(
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();
var organization = fixture.Create<Organization>();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns((OrganizationReport)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
Assert.Equal("Organization report not found", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportSummaryAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns(existingReport);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
Assert.Equal("Organization report does not belong to the specified organization", exception.Message);
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationReportSummaryAsync_WhenRepositoryThrowsException_ShouldPropagateException(
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();
var organization = fixture.Create<Organization>();
var existingReport = fixture.Build<OrganizationReport>()
.With(x => x.Id, request.ReportId)
.With(x => x.OrganizationId, request.OrganizationId)
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(request.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationReportRepository>()
.GetByIdAsync(request.ReportId)
.Returns(existingReport);
sutProvider.GetDependency<IOrganizationReportRepository>()
.UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData)
.Throws(new InvalidOperationException("Database connection failed"));
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
Assert.Equal("Database connection failed", exception.Message);
}
}

View File

@@ -22,18 +22,18 @@ using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Platform.Push.Services;
namespace Bit.Core.Test.Platform.Push.Engines;
[QueueClientCustomize]
[SutProviderCustomize]
public class AzureQueuePushNotificationServiceTests
public class AzureQueuePushEngineTests
{
private static readonly Guid _deviceId = Guid.Parse("c4730f80-caaa-4772-97bd-5c0d23a2baa3");
private static readonly string _deviceIdentifier = "test_device_identifier";
private readonly FakeTimeProvider _fakeTimeProvider;
private readonly Core.Settings.GlobalSettings _globalSettings = new();
public AzureQueuePushNotificationServiceTests()
public AzureQueuePushEngineTests()
{
_fakeTimeProvider = new();
_fakeTimeProvider.SetUtcNow(DateTime.UtcNow);
@@ -651,31 +651,6 @@ public class AzureQueuePushNotificationServiceTests
);
}
[Fact]
public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse()
{
var organization = new Organization
{
Id = Guid.NewGuid(),
Enabled = true,
};
var expectedPayload = new JsonObject
{
["Type"] = 18,
["Payload"] = new JsonObject
{
["OrganizationId"] = organization.Id,
["Enabled"] = organization.Enabled,
},
};
await VerifyNotificationAsync(
async sut => await sut.PushSyncOrganizationStatusAsync(organization),
expectedPayload
);
}
[Fact]
public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()
{
@@ -771,12 +746,11 @@ public class AzureQueuePushNotificationServiceTests
var globalSettings = new Core.Settings.GlobalSettings();
var sut = new AzureQueuePushNotificationService(
var sut = new AzureQueuePushEngine(
queueClient,
httpContextAccessor,
globalSettings,
NullLogger<AzureQueuePushNotificationService>.Instance,
_fakeTimeProvider
NullLogger<AzureQueuePushEngine>.Instance
);
await test(new EngineWrapper(sut, _fakeTimeProvider, _globalSettings.Installation.Id));

View File

@@ -2,16 +2,16 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.Extensions.Logging.Abstractions;
namespace Bit.Core.Test.Platform.Push.Services;
namespace Bit.Core.Test.Platform.Push.Engines;
public class NotificationsApiPushNotificationServiceTests : PushTestBase
public class NotificationsApiPushEngineTests : PushTestBase
{
public NotificationsApiPushNotificationServiceTests()
public NotificationsApiPushEngineTests()
{
GlobalSettings.BaseServiceUri.InternalNotifications = "https://localhost:7777";
GlobalSettings.BaseServiceUri.InternalIdentity = "https://localhost:8888";
@@ -21,11 +21,11 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase
protected override IPushEngine CreateService()
{
return new NotificationsApiPushNotificationService(
return new NotificationsApiPushEngine(
HttpClientFactory,
GlobalSettings,
HttpContextAccessor,
NullLogger<NotificationsApiPushNotificationService>.Instance
NullLogger<NotificationsApiPushEngine>.Instance
);
}

View File

@@ -10,6 +10,7 @@ using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
@@ -22,6 +23,8 @@ using NSubstitute;
using RichardSzalay.MockHttp;
using Xunit;
namespace Bit.Core.Test.Platform.Push.Engines;
public class EngineWrapper(IPushEngine pushEngine, FakeTimeProvider fakeTimeProvider, Guid installationId) : IPushNotificationService
{
public Guid InstallationId { get; } = installationId;
@@ -410,21 +413,6 @@ public abstract class PushTestBase
);
}
[Fact]
public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse()
{
var organization = new Organization
{
Id = Guid.NewGuid(),
Enabled = true,
};
await VerifyNotificationAsync(
async sut => await sut.PushSyncOrganizationStatusAsync(organization),
GetPushSyncOrganizationStatusResponsePayload(organization)
);
}
[Fact]
public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()
{

View File

@@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Entities;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Repositories;
using Bit.Core.Settings;
@@ -15,7 +14,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
namespace Bit.Core.Test.Platform.Push.Services;
namespace Bit.Core.Test.Platform.Push.Engines;
public class RelayPushNotificationServiceTests : PushTestBase
{
@@ -39,12 +38,12 @@ public class RelayPushNotificationServiceTests : PushTestBase
protected override IPushEngine CreateService()
{
return new RelayPushNotificationService(
return new RelayPushEngine(
HttpClientFactory,
_deviceRepository,
GlobalSettings,
HttpContextAccessor,
NullLogger<RelayPushNotificationService>.Instance
NullLogger<RelayPushEngine>.Instance
);
}

View File

@@ -0,0 +1,57 @@
using Bit.Core.Enums;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Platform.Push;
public class MultiServicePushNotificationServiceTests
{
private readonly IPushEngine _fakeEngine1;
private readonly IPushEngine _fakeEngine2;
private readonly MultiServicePushNotificationService _sut;
public MultiServicePushNotificationServiceTests()
{
_fakeEngine1 = Substitute.For<IPushEngine>();
_fakeEngine2 = Substitute.For<IPushEngine>();
_sut = new MultiServicePushNotificationService(
[_fakeEngine1, _fakeEngine2],
NullLogger<MultiServicePushNotificationService>.Instance,
new GlobalSettings(),
new FakeTimeProvider()
);
}
#if DEBUG // This test requires debug code in the sut to work properly
[Fact]
public async Task PushAsync_CallsAllEngines()
{
var notification = new PushNotification<object>
{
Target = NotificationTarget.User,
TargetId = Guid.NewGuid(),
Type = PushType.AuthRequest,
Payload = new { },
ExcludeCurrentContext = false,
};
await _sut.PushAsync(notification);
await _fakeEngine1
.Received(1)
.PushAsync(Arg.Is<PushNotification<object>>(n => ReferenceEquals(n, notification)));
await _fakeEngine2
.Received(1)
.PushAsync(Arg.Is<PushNotification<object>>(n => ReferenceEquals(n, notification)));
}
#endif
}

View File

@@ -1,9 +1,9 @@
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Xunit;
namespace Bit.Core.Test.NotificationHub;
namespace Bit.Core.Test.Platform.Push.NotificationHub;
public class NotificationHubConnectionTests
{

View File

@@ -1,4 +1,4 @@
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
@@ -6,7 +6,7 @@ using NSubstitute;
using Xunit;
using static Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.NotificationHub;
namespace Bit.Core.Test.Platform.Push.NotificationHub;
public class NotificationHubPoolTests
{

View File

@@ -1,11 +1,11 @@
using AutoFixture;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Push.Internal;
using Bit.Test.Common.AutoFixture;
using Microsoft.Azure.NotificationHubs;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.NotificationHub;
namespace Bit.Core.Test.Platform.Push.NotificationHub;
public class NotificationHubProxyTests
{

View File

@@ -1,5 +1,4 @@
#nullable enable
using System.Text.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using Bit.Core.Auth.Entities;
using Bit.Core.Context;
@@ -7,10 +6,11 @@ using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Repositories;
using Bit.Core.Test.NotificationCenter.AutoFixture;
using Bit.Core.Test.Platform.Push.Engines;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Bit.Test.Common.AutoFixture;
@@ -22,7 +22,7 @@ using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.NotificationHub;
namespace Bit.Core.Test.Platform.Push.NotificationHub;
[SutProviderCustomize]
[NotificationStatusCustomize]
@@ -621,11 +621,11 @@ public class NotificationHubPushNotificationServiceTests
fakeTimeProvider.SetUtcNow(_now);
var sut = new NotificationHubPushNotificationService(
var sut = new NotificationHubPushEngine(
installationDeviceRepository,
notificationHubPool,
httpContextAccessor,
NullLogger<NotificationHubPushNotificationService>.Instance,
NullLogger<NotificationHubPushEngine>.Instance,
globalSettings
);
@@ -676,7 +676,7 @@ public class NotificationHubPushNotificationServiceTests
};
private static async Task AssertSendTemplateNotificationAsync(
SutProvider<NotificationHubPushNotificationService> sutProvider, PushType type, object payload, string tag)
SutProvider<NotificationHubPushEngine> sutProvider, PushType type, object payload, string tag)
{
await sutProvider.GetDependency<INotificationHubPool>()
.Received(1)

View File

@@ -0,0 +1,198 @@
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Repositories;
using Bit.Core.Repositories.Noop;
using Bit.Core.Settings;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xunit;
namespace Bit.Core.Test.Platform.Push;
public class PushServiceCollectionExtensionsTests
{
[Fact]
public void AddPush_SelfHosted_NoConfig_NoEngines()
{
var services = Build(new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "true" },
{ "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() },
});
_ = services.GetRequiredService<IPushNotificationService>();
var engines = services.GetServices<IPushEngine>();
Assert.Empty(engines);
}
[Fact]
public void AddPush_SelfHosted_ConfiguredForRelay_RelayEngineAdded()
{
var services = Build(new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "true" },
{ "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() },
{ "GlobalSettings:Installation:Key", "some_key"},
{ "GlobalSettings:PushRelayBaseUri", "https://example.com" },
});
_ = services.GetRequiredService<IPushNotificationService>();
var engines = services.GetServices<IPushEngine>();
var engine = Assert.Single(engines);
Assert.IsType<RelayPushEngine>(engine);
}
[Fact]
public void AddPush_SelfHosted_ConfiguredForApi_ApiEngineAdded()
{
var services = Build(new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "true" },
{ "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() },
{ "GlobalSettings:InternalIdentityKey", "some_key"},
{ "GlobalSettings:BaseServiceUri", "https://example.com" },
});
_ = services.GetRequiredService<IPushNotificationService>();
var engines = services.GetServices<IPushEngine>();
var engine = Assert.Single(engines);
Assert.IsType<NotificationsApiPushEngine>(engine);
}
[Fact]
public void AddPush_SelfHosted_ConfiguredForRelayAndApi_TwoEnginesAdded()
{
var services = Build(new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "true" },
{ "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() },
{ "GlobalSettings:Installation:Key", "some_key"},
{ "GlobalSettings:PushRelayBaseUri", "https://example.com" },
{ "GlobalSettings:InternalIdentityKey", "some_key"},
{ "GlobalSettings:BaseServiceUri", "https://example.com" },
});
_ = services.GetRequiredService<IPushNotificationService>();
var engines = services.GetServices<IPushEngine>();
Assert.Collection(
engines,
e => Assert.IsType<RelayPushEngine>(e),
e => Assert.IsType<NotificationsApiPushEngine>(e)
);
}
[Fact]
public void AddPush_Cloud_NoConfig_AddsNotificationHub()
{
var services = Build(new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "false" },
});
_ = services.GetRequiredService<IPushNotificationService>();
var engines = services.GetServices<IPushEngine>();
var engine = Assert.Single(engines);
Assert.IsType<NotificationHubPushEngine>(engine);
}
[Fact]
public void AddPush_Cloud_HasNotificationConnectionString_TwoEngines()
{
var services = Build(new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "false" },
{ "GlobalSettings:Notifications:ConnectionString", "UseDevelopmentStorage=true" },
});
_ = services.GetRequiredService<IPushNotificationService>();
var engines = services.GetServices<IPushEngine>();
Assert.Collection(
engines,
e => Assert.IsType<NotificationHubPushEngine>(e),
e => Assert.IsType<AzureQueuePushEngine>(e)
);
}
[Fact]
public void AddPush_Cloud_CalledTwice_DoesNotAddServicesTwice()
{
var services = new ServiceCollection();
var config = new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "false" },
{ "GlobalSettings:Notifications:ConnectionString", "UseDevelopmentStorage=true" },
};
AddServices(services, config);
var initialCount = services.Count;
// Add services again
AddServices(services, config);
Assert.Equal(initialCount, services.Count);
}
private static ServiceProvider Build(Dictionary<string, string?> initialData)
{
var services = new ServiceCollection();
AddServices(services, initialData);
return services.BuildServiceProvider();
}
private static void AddServices(IServiceCollection services, Dictionary<string, string?> initialData)
{
// A minimal service collection is always expected to have logging, config, and global settings
// pre-registered.
services.AddLogging();
var config = new ConfigurationBuilder()
.AddInMemoryCollection(initialData)
.Build();
services.TryAddSingleton(config);
var globalSettings = new GlobalSettings();
config.GetSection("GlobalSettings").Bind(globalSettings);
services.TryAddSingleton(globalSettings);
services.TryAddSingleton<IGlobalSettings>(globalSettings);
// Temporary until AddPush can add it themselves directly.
services.TryAddSingleton<IDeviceRepository, StubDeviceRepository>();
// Temporary until AddPush can add it themselves directly.
services.TryAddSingleton<IInstallationDeviceRepository, InstallationDeviceRepository>();
services.AddPush(globalSettings);
}
private class StubDeviceRepository : IDeviceRepository
{
public Task ClearPushTokenAsync(Guid id) => throw new NotImplementedException();
public Task<Device> CreateAsync(Device obj) => throw new NotImplementedException();
public Task DeleteAsync(Device obj) => throw new NotImplementedException();
public Task<Device?> GetByIdAsync(Guid id, Guid userId) => throw new NotImplementedException();
public Task<Device?> GetByIdAsync(Guid id) => throw new NotImplementedException();
public Task<Device?> GetByIdentifierAsync(string identifier) => throw new NotImplementedException();
public Task<Device?> GetByIdentifierAsync(string identifier, Guid userId) => throw new NotImplementedException();
public Task<ICollection<Device>> GetManyByUserIdAsync(Guid userId) => throw new NotImplementedException();
public Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId) => throw new NotImplementedException();
public Task ReplaceAsync(Device obj) => throw new NotImplementedException();
public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<Device> devices) => throw new NotImplementedException();
public Task UpsertAsync(Device obj) => throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,64 @@
using System.Diagnostics;
using System.Reflection;
using Bit.Core.Enums;
using Bit.Core.Platform.Push;
using Xunit;
namespace Bit.Core.Test.Platform.Push;
public class PushTypeTests
{
[Fact]
public void AllEnumMembersHaveUniqueValue()
{
// No enum member should use the same value as another named member.
var usedNumbers = new HashSet<byte>();
var enumMembers = Enum.GetValues<PushType>();
foreach (var enumMember in enumMembers)
{
if (!usedNumbers.Add((byte)enumMember))
{
Assert.Fail($"Enum number value ({(byte)enumMember}) on {enumMember} is already in use.");
}
}
}
[Fact]
public void AllEnumMembersHaveNotificationInfoAttribute()
{
// Every enum member should be annotated with [NotificationInfo]
foreach (var member in typeof(PushType).GetMembers(BindingFlags.Public | BindingFlags.Static))
{
var notificationInfoAttribute = member.GetCustomAttribute<NotificationInfoAttribute>();
if (notificationInfoAttribute is null)
{
Assert.Fail($"PushType.{member.Name} is missing a required [NotificationInfo(\"team-name\", typeof(MyType))] attribute.");
}
}
}
[Fact]
public void AllEnumValuesAreInSequence()
{
// There should not be any gaps in the numbers defined for an enum, that being if someone last defined 22
// the next number used should be 23 not 24 or any other number.
var sortedValues = Enum.GetValues<PushType>()
.Order()
.ToArray();
Debug.Assert(sortedValues.Length > 0);
var lastValue = sortedValues[0];
foreach (var value in sortedValues[1..])
{
var expectedValue = ++lastValue;
Assert.Equal(expectedValue, value);
}
}
}

View File

@@ -1,8 +0,0 @@
#nullable enable
namespace Bit.Core.Test.Platform.Push.Services;
public class MultiServicePushNotificationServiceTests
{
// TODO: Can add a couple tests here
}

View File

@@ -1,6 +1,8 @@
#nullable enable
using Bit.Core.Enums;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Platform.PushRegistration;
using Bit.Core.Platform.PushRegistration.Internal;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;

View File

@@ -0,0 +1,108 @@
using Bit.Core.Platform.Push;
using Bit.Core.Platform.PushRegistration.Internal;
using Bit.Core.Repositories;
using Bit.Core.Repositories.Noop;
using Bit.Core.Settings;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xunit;
namespace Bit.Core.Test.Platform.PushRegistration;
public class PushRegistrationServiceCollectionExtensionsTests
{
[Fact]
public void AddPushRegistration_Cloud_CreatesNotificationHubRegistrationService()
{
var services = Build(new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "false" },
});
var pushRegistrationService = services.GetRequiredService<IPushRegistrationService>();
Assert.IsType<NotificationHubPushRegistrationService>(pushRegistrationService);
}
[Fact]
public void AddPushRegistration_SelfHosted_NoOtherConfig_ReturnsNoopRegistrationService()
{
var services = Build(new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "true" },
});
var pushRegistrationService = services.GetRequiredService<IPushRegistrationService>();
Assert.IsType<NoopPushRegistrationService>(pushRegistrationService);
}
[Fact]
public void AddPushRegistration_SelfHosted_RelayConfig_ReturnsRelayRegistrationService()
{
var services = Build(new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "true" },
{ "GlobalSettings:PushRelayBaseUri", "https://example.com" },
{ "GlobalSettings:Installation:Key", "some_key" },
});
var pushRegistrationService = services.GetRequiredService<IPushRegistrationService>();
Assert.IsType<RelayPushRegistrationService>(pushRegistrationService);
}
[Fact]
public void AddPushRegistration_MultipleTimes_NoAdditionalServices()
{
var services = new ServiceCollection();
var config = new Dictionary<string, string?>
{
{ "GlobalSettings:SelfHosted", "true" },
{ "GlobalSettings:PushRelayBaseUri", "https://example.com" },
{ "GlobalSettings:Installation:Key", "some_key" },
};
AddServices(services, config);
// Add services again
services.AddPushRegistration();
var provider = services.BuildServiceProvider();
Assert.Single(provider.GetServices<IPushRegistrationService>());
}
private static ServiceProvider Build(Dictionary<string, string?> initialData)
{
var services = new ServiceCollection();
AddServices(services, initialData);
return services.BuildServiceProvider();
}
private static void AddServices(IServiceCollection services, Dictionary<string, string?> initialData)
{
// A minimal service collection is always expected to have logging, config, and global settings
// pre-registered.
services.AddLogging();
var config = new ConfigurationBuilder()
.AddInMemoryCollection(initialData)
.Build();
services.TryAddSingleton(config);
var globalSettings = new GlobalSettings();
config.GetSection("GlobalSettings").Bind(globalSettings);
services.TryAddSingleton(globalSettings);
services.TryAddSingleton<IGlobalSettings>(globalSettings);
// Temporary until AddPushRegistration can add it themselves directly.
services.TryAddSingleton<IInstallationDeviceRepository, InstallationDeviceRepository>();
services.AddPushRegistration();
}
}

View File

@@ -1,4 +1,4 @@
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Platform.PushRegistration.Internal;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using NSubstitute;

View File

@@ -4,8 +4,8 @@ using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.PushRegistration;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;

View File

@@ -247,11 +247,18 @@ public class HandlebarsMailServiceTests
}
}
// Remove this test when we add actual tests. It only proves that
// we've properly constructed the system under test.
[Fact]
public void ServiceExists()
public async Task SendSendEmailOtpEmailAsync_SendsEmail()
{
Assert.NotNull(_sut);
// Arrange
var email = "test@example.com";
var token = "aToken";
var subject = string.Format("Your Bitwarden Send verification code is {0}", token);
// Act
await _sut.SendSendEmailOtpEmailAsync(email, token, subject);
// Assert
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any<MailMessage>());
}
}

View File

@@ -0,0 +1,541 @@
using Bit.Core.AdminConsole.AbilitiesCache;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Services.Implementations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.Services.Implementations;
[SutProviderCustomize]
public class FeatureRoutedCacheServiceTests
{
[Theory, BitAutoData]
public async Task GetOrganizationAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
SutProvider<FeatureRoutedCacheService> sutProvider,
IDictionary<Guid, OrganizationAbility> expectedResult)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(true);
sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.GetOrganizationAbilitiesAsync()
.Returns(expectedResult);
// Act
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Assert
Assert.Equal(expectedResult, result);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.Received(1)
.GetOrganizationAbilitiesAsync();
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.DidNotReceive()
.GetOrganizationAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilitiesAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService(
SutProvider<FeatureRoutedCacheService> sutProvider,
IDictionary<Guid, OrganizationAbility> expectedResult)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.GetOrganizationAbilitiesAsync()
.Returns(expectedResult);
// Act
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Assert
Assert.Equal(expectedResult, result);
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.GetOrganizationAbilitiesAsync();
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.GetOrganizationAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilityAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Guid orgId,
OrganizationAbility expectedResult)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(true);
sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.GetOrganizationAbilityAsync(orgId)
.Returns(expectedResult);
// Act
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(orgId);
// Assert
Assert.Equal(expectedResult, result);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.Received(1)
.GetOrganizationAbilityAsync(orgId);
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.DidNotReceive()
.GetOrganizationAbilityAsync(orgId);
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilityAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Guid orgId,
OrganizationAbility expectedResult)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.GetOrganizationAbilityAsync(orgId)
.Returns(expectedResult);
// Act
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(orgId);
// Assert
Assert.Equal(expectedResult, result);
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.GetOrganizationAbilityAsync(orgId);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.GetOrganizationAbilityAsync(orgId);
}
[Theory, BitAutoData]
public async Task GetProviderAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
SutProvider<FeatureRoutedCacheService> sutProvider,
IDictionary<Guid, ProviderAbility> expectedResult)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(true);
sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.GetProviderAbilitiesAsync()
.Returns(expectedResult);
// Act
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
// Assert
Assert.Equal(expectedResult, result);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.Received(1)
.GetProviderAbilitiesAsync();
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.DidNotReceive()
.GetProviderAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetProviderAbilitiesAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService(
SutProvider<FeatureRoutedCacheService> sutProvider,
IDictionary<Guid, ProviderAbility> expectedResult)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.GetProviderAbilitiesAsync()
.Returns(expectedResult);
// Act
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
// Assert
Assert.Equal(expectedResult, result);
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.GetProviderAbilitiesAsync();
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.GetProviderAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Organization organization)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(true);
// Act
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
// Assert
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.Received(1)
.UpsertOrganizationAbilityAsync(organization);
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.DidNotReceive()
.GetProviderAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Organization organization)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
// Act
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
// Assert
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.UpsertOrganizationAbilityAsync(organization);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.GetProviderAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task UpsertProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Provider provider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(true);
// Act
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
// Assert
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.Received(1)
.UpsertProviderAbilityAsync(provider);
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.DidNotReceive()
.UpsertProviderAbilityAsync(provider);
}
[Theory, BitAutoData]
public async Task UpsertProviderAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Provider provider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
// Act
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
// Assert
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.UpsertProviderAbilityAsync(provider);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.UpsertProviderAbilityAsync(provider);
}
[Theory, BitAutoData]
public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Guid organizationId)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(true);
// Act
await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId);
// Assert
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.Received(1)
.DeleteOrganizationAbilityAsync(organizationId);
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.DidNotReceive()
.DeleteOrganizationAbilityAsync(organizationId);
}
[Theory, BitAutoData]
public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Guid organizationId)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
// Act
await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId);
// Assert
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.DeleteOrganizationAbilityAsync(organizationId);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.DeleteOrganizationAbilityAsync(organizationId);
}
[Theory, BitAutoData]
public async Task DeleteProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Guid providerId)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(true);
// Act
await sutProvider.Sut.DeleteProviderAbilityAsync(providerId);
// Assert
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.Received(1)
.DeleteProviderAbilityAsync(providerId);
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.DidNotReceive()
.DeleteProviderAbilityAsync(providerId);
}
[Theory, BitAutoData]
public async Task DeleteProviderAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Guid providerId)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
// Act
await sutProvider.Sut.DeleteProviderAbilityAsync(providerId);
// Assert
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.Received(1)
.DeleteProviderAbilityAsync(providerId);
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.DidNotReceive()
.DeleteProviderAbilityAsync(providerId);
}
[Theory, BitAutoData]
public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Organization organization)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(true);
// Act
await sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization);
// Assert
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.Received(1)
.UpsertOrganizationAbilityAsync(organization);
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.DidNotReceive()
.UpsertOrganizationAbilityAsync(organization);
}
[Theory, BitAutoData]
public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsServiceBusCache(
Organization organization)
{
// Arrange
var featureService = Substitute.For<IFeatureService>();
var currentCacheService = CreateCurrentCacheMockService();
featureService
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
var sutProvider = Substitute.For<FeatureRoutedCacheService>(
featureService,
Substitute.For<IVNextInMemoryApplicationCacheService>(),
currentCacheService,
Substitute.For<IApplicationCacheServiceBusMessaging>());
// Act
await sutProvider.BaseUpsertOrganizationAbilityAsync(organization);
// Assert
await currentCacheService
.Received(1)
.BaseUpsertOrganizationAbilityAsync(organization);
}
/// <summary>
/// Our SUT is using a method that is not part of the IVCurrentInMemoryApplicationCacheService,
/// so AutoFixtures auto-created mock wont work.
/// </summary>
/// <returns></returns>
private static InMemoryServiceBusApplicationCacheService CreateCurrentCacheMockService()
{
var currentCacheService = Substitute.For<InMemoryServiceBusApplicationCacheService>(
Substitute.For<IOrganizationRepository>(),
Substitute.For<IProviderRepository>(),
new GlobalSettings
{
ProjectName = "BitwardenTest",
ServiceBus = new GlobalSettings.ServiceBusSettings
{
ConnectionString = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
ApplicationCacheTopicName = "test-topic",
ApplicationCacheSubscriptionName = "test-subscription"
}
});
return currentCacheService;
}
[Theory, BitAutoData]
public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_AndServiceIsNotServiceBusCache_ThrowsException(
SutProvider<FeatureRoutedCacheService> sutProvider,
Organization organization)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization));
// Assert
Assert.Equal(
ExpectedErrorMessage,
ex.Message);
}
private static string ExpectedErrorMessage
{
get => "Expected inMemoryApplicationCacheService to be of type InMemoryServiceBusApplicationCacheService";
}
[Theory, BitAutoData]
public async Task BaseDeleteOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
SutProvider<FeatureRoutedCacheService> sutProvider,
Guid organizationId)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(true);
// Act
await sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId);
// Assert
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
.Received(1)
.DeleteOrganizationAbilityAsync(organizationId);
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
.DidNotReceive()
.DeleteOrganizationAbilityAsync(organizationId);
}
[Theory, BitAutoData]
public async Task BaseDeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsServiceBusCache(
Guid organizationId)
{
// Arrange
var featureService = Substitute.For<IFeatureService>();
var currentCacheService = CreateCurrentCacheMockService();
featureService
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
var sutProvider = Substitute.For<FeatureRoutedCacheService>(
featureService,
Substitute.For<IVNextInMemoryApplicationCacheService>(),
currentCacheService,
Substitute.For<IApplicationCacheServiceBusMessaging>());
// Act
await sutProvider.BaseDeleteOrganizationAbilityAsync(organizationId);
// Assert
await currentCacheService
.Received(1)
.BaseDeleteOrganizationAbilityAsync(organizationId);
}
[Theory, BitAutoData]
public async Task
BaseDeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_AndServiceIsNotServiceBusCache_ThrowsException(
SutProvider<FeatureRoutedCacheService> sutProvider,
Guid organizationId)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
.Returns(false);
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId));
// Assert
Assert.Equal(
ExpectedErrorMessage,
ex.Message);
}
}

View File

@@ -1,12 +1,10 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Billing.Tax.Requests;
using Bit.Core.Billing.Tax.Services;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Tax.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@@ -23,10 +21,6 @@ public class StripePaymentServiceTests
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
sutProvider.GetDependency<IAutomaticTaxFactory>()
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
.Returns(new FakeAutomaticTaxStrategy(true));
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
@@ -74,10 +68,6 @@ public class StripePaymentServiceTests
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
sutProvider.GetDependency<IAutomaticTaxFactory>()
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
.Returns(new FakeAutomaticTaxStrategy(true));
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
@@ -125,10 +115,6 @@ public class StripePaymentServiceTests
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
sutProvider.GetDependency<IAutomaticTaxFactory>()
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
.Returns(new FakeAutomaticTaxStrategy(true));
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
@@ -177,10 +163,6 @@ public class StripePaymentServiceTests
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
sutProvider.GetDependency<IAutomaticTaxFactory>()
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
.Returns(new FakeAutomaticTaxStrategy(true));
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
@@ -223,4 +205,340 @@ public class StripePaymentServiceTests
Assert.Equal(4.08M, actual.TotalAmount);
Assert.Equal(4M, actual.TaxableBaseAmount);
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_USBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(familiesPlan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "US",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_USBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var plan = new EnterprisePlan(true);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
.Returns(plan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.EnterpriseAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "US",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(familiesPlan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var plan = new EnterprisePlan(true);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
.Returns(plan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.EnterpriseAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_USBased_PersonalUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(familiesPlan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "US",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_USBased_BusinessUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var plan = new EnterprisePlan(true);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
.Returns(plan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.EnterpriseAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "US",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(familiesPlan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsTaxExemptReverse(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var plan = new EnterprisePlan(true);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
.Returns(plan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.EnterpriseAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse
));
}
}

View File

@@ -1,5 +1,6 @@
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;
@@ -46,7 +47,41 @@ public class ImportCiphersAsyncCommandTests
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
// Assert
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(importingUserId, 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);
}
[Theory, BitAutoData]
public async Task ImportIntoIndividualVaultAsync_WithBulkResourceCreationServiceEnabled_Success(
Guid importingUserId,
List<CipherDetails> ciphers,
SutProvider<ImportCiphersCommand> sutProvider)
{
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
.Returns(true);
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership)
.Returns(false);
sutProvider.GetDependency<IFolderRepository>()
.GetManyByUserIdAsync(importingUserId)
.Returns(new List<Folder>());
var folders = new List<Folder> { new Folder { UserId = importingUserId } };
var folderRelationships = new List<KeyValuePair<int, int>>();
// Act
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
// Assert
await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.CreateAsync_vNext(importingUserId, ciphers, Arg.Any<List<Folder>>());
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
}
@@ -76,7 +111,45 @@ public class ImportCiphersAsyncCommandTests
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(importingUserId, 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);
}
[Theory, BitAutoData]
public async Task ImportIntoIndividualVaultAsync_WithBulkResourceCreationServiceEnabled_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Success(
Guid importingUserId,
List<CipherDetails> ciphers,
SutProvider<ImportCiphersCommand> sutProvider)
{
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
.Returns(true);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(importingUserId)
.Returns(new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Disabled,
[]));
sutProvider.GetDependency<IFolderRepository>()
.GetManyByUserIdAsync(importingUserId)
.Returns(new List<Folder>());
var folders = new List<Folder> { new Folder { UserId = importingUserId } };
var folderRelationships = new List<KeyValuePair<int, int>>();
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.CreateAsync_vNext(importingUserId, ciphers, Arg.Any<List<Folder>>());
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
}
@@ -120,7 +193,7 @@ public class ImportCiphersAsyncCommandTests
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
.Returns(new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Enabled,
[Guid.NewGuid()]));
[new PolicyDetails()]));
var folderRelationships = new List<KeyValuePair<int, int>>();
@@ -186,6 +259,66 @@ public class ImportCiphersAsyncCommandTests
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
}
[Theory, BitAutoData]
public async Task ImportIntoOrganizationalVaultAsync_WithBulkResourceCreationServiceEnabled_Success(
Organization organization,
Guid importingUserId,
OrganizationUser importingOrganizationUser,
List<Collection> collections,
List<CipherDetails> ciphers,
SutProvider<ImportCiphersCommand> sutProvider)
{
organization.MaxCollections = null;
importingOrganizationUser.OrganizationId = organization.Id;
foreach (var collection in collections)
{
collection.OrganizationId = organization.Id;
}
foreach (var cipher in ciphers)
{
cipher.OrganizationId = organization.Id;
}
KeyValuePair<int, int>[] collectionRelationships = {
new(0, 0),
new(1, 1),
new(2, 2)
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organization.Id, importingUserId)
.Returns(importingOrganizationUser);
// Set up a collection that already exists in the organization
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByOrganizationIdAsync(organization.Id)
.Returns(new List<Collection> { collections[0] });
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync_vNext(
ciphers,
Arg.Is<IEnumerable<Collection>>(cols => cols.Count() == collections.Count - 1 &&
!cols.Any(c => c.Id == collections[0].Id) && // Check that the collection that already existed in the organization was not added
cols.All(c => collections.Any(x => c.Name == x.Name))),
Arg.Is<IEnumerable<CollectionCipher>>(c => c.Count() == ciphers.Count),
Arg.Is<IEnumerable<CollectionUser>>(cus =>
cus.Count() == collections.Count - 1 &&
!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);
}
[Theory, BitAutoData]
public async Task ImportIntoOrganizationalVaultAsync_ThrowsBadRequestException(
Organization organization,

View File

@@ -12,9 +12,11 @@ internal class OrganizationCipher : ICustomization
{
fixture.Customize<Cipher>(composer => composer
.With(c => c.OrganizationId, OrganizationId ?? Guid.NewGuid())
.Without(c => c.ArchivedDate)
.Without(c => c.UserId));
fixture.Customize<CipherDetails>(composer => composer
.With(c => c.OrganizationId, Guid.NewGuid())
.Without(c => c.ArchivedDate)
.Without(c => c.UserId));
}
}
@@ -26,9 +28,11 @@ internal class UserCipher : ICustomization
{
fixture.Customize<Cipher>(composer => composer
.With(c => c.UserId, UserId ?? Guid.NewGuid())
.Without(c => c.ArchivedDate)
.Without(c => c.OrganizationId));
fixture.Customize<CipherDetails>(composer => composer
.With(c => c.UserId, Guid.NewGuid())
.Without(c => c.ArchivedDate)
.Without(c => c.OrganizationId));
}
}

View File

@@ -0,0 +1,49 @@
using Bit.Core.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Test.AutoFixture.CipherFixtures;
using Bit.Core.Vault.Commands;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Vault.Commands;
[UserCipherCustomize]
[SutProviderCustomize]
public class ArchiveCiphersCommandTest
{
[Theory]
[BitAutoData(true, false, 1, 1, 1)]
[BitAutoData(false, false, 1, 0, 1)]
[BitAutoData(false, true, 1, 0, 1)]
[BitAutoData(true, true, 1, 0, 1)]
public async Task ArchiveAsync_Works(
bool isEditable, bool hasOrganizationId,
int cipherRepoCalls, int resultCountFromQuery, int pushNotificationsCalls,
SutProvider<ArchiveCiphersCommand> sutProvider, CipherDetails cipher, User user)
{
cipher.Edit = isEditable;
cipher.OrganizationId = hasOrganizationId ? Guid.NewGuid() : null;
var cipherList = new List<CipherDetails> { cipher };
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(user.Id).Returns(cipherList);
// Act
await sutProvider.Sut.ArchiveManyAsync([cipher.Id], user.Id);
// Assert
await sutProvider.GetDependency<ICipherRepository>().Received(cipherRepoCalls).ArchiveAsync(
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == resultCountFromQuery
&& ids.Count() >= 1
? true
: ids.All(id => cipherList.Contains(cipher))),
user.Id);
await sutProvider.GetDependency<IPushNotificationService>().Received(pushNotificationsCalls)
.PushSyncCiphersAsync(user.Id);
}
}

View File

@@ -0,0 +1,49 @@
using Bit.Core.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Test.AutoFixture.CipherFixtures;
using Bit.Core.Vault.Commands;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Vault.Commands;
[UserCipherCustomize]
[SutProviderCustomize]
public class UnarchiveCiphersCommandTest
{
[Theory]
[BitAutoData(true, false, 1, 1, 1)]
[BitAutoData(false, false, 1, 0, 1)]
[BitAutoData(false, true, 1, 0, 1)]
[BitAutoData(true, true, 1, 1, 1)]
public async Task UnarchiveAsync_Works(
bool isEditable, bool hasOrganizationId,
int cipherRepoCalls, int resultCountFromQuery, int pushNotificationsCalls,
SutProvider<UnarchiveCiphersCommand> sutProvider, CipherDetails cipher, User user)
{
cipher.Edit = isEditable;
cipher.OrganizationId = hasOrganizationId ? Guid.NewGuid() : null;
var cipherList = new List<CipherDetails> { cipher };
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(user.Id).Returns(cipherList);
// Act
await sutProvider.Sut.UnarchiveManyAsync([cipher.Id], user.Id);
// Assert
await sutProvider.GetDependency<ICipherRepository>().Received(cipherRepoCalls).UnarchiveAsync(
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == resultCountFromQuery
&& ids.Count() >= 1
? true
: ids.All(id => cipherList.Contains(cipher))),
user.Id);
await sutProvider.GetDependency<IPushNotificationService>().Received(pushNotificationsCalls)
.PushSyncCiphersAsync(user.Id);
}
}

View File

@@ -89,4 +89,47 @@ public class OrganizationCiphersQueryTests
c.CollectionIds.Any(cId => cId == targetCollectionId) &&
c.CollectionIds.Any(cId => cId == otherCollectionId));
}
[Theory, BitAutoData]
public async Task GetAllOrganizationCiphersExcludingDefaultUserCollections_DelegatesToRepository(
Guid organizationId,
SutProvider<OrganizationCiphersQuery> sutProvider)
{
var item1 = new CipherOrganizationDetailsWithCollections(
new CipherOrganizationDetails { Id = Guid.NewGuid(), OrganizationId = organizationId },
new Dictionary<Guid, IGrouping<Guid, CollectionCipher>>());
var item2 = new CipherOrganizationDetailsWithCollections(
new CipherOrganizationDetails { Id = Guid.NewGuid(), OrganizationId = organizationId },
new Dictionary<Guid, IGrouping<Guid, CollectionCipher>>());
var repo = sutProvider.GetDependency<ICipherRepository>();
repo.GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(organizationId)
.Returns(Task.FromResult<IEnumerable<CipherOrganizationDetailsWithCollections>>(
new[] { item1, item2 }));
var actual = (await sutProvider.Sut
.GetAllOrganizationCiphersExcludingDefaultUserCollections(organizationId))
.ToList();
Assert.Equal(2, actual.Count);
Assert.Same(item1, actual[0]);
Assert.Same(item2, actual[1]);
// and we indeed called the repo once
await repo.Received(1)
.GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(organizationId);
}
private CipherOrganizationDetailsWithCollections MakeWith(
CipherOrganizationDetails baseCipher,
params Guid[] cols)
{
var dict = cols
.Select(cid => new CollectionCipher { CipherId = baseCipher.Id, CollectionId = cid })
.GroupBy(cc => cc.CipherId)
.ToDictionary(g => g.Key, g => g);
return new CipherOrganizationDetailsWithCollections(baseCipher, dict);
}
}

View File

@@ -1,6 +1,7 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
@@ -173,7 +174,7 @@ public class CipherServiceTests
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(savingUserId)
.Returns(new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Enabled,
[Guid.NewGuid()]));
[new PolicyDetails()]));
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null));
@@ -673,6 +674,32 @@ public class CipherServiceTests
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}
[Theory]
[BitAutoData("")]
[BitAutoData("Correct Time")]
public async Task ShareManyAsync_CorrectRevisionDate_WithBulkResourceCreationServiceEnabled_Passes(string revisionDateString,
SutProvider<CipherService> sutProvider, IEnumerable<CipherDetails> ciphers, Organization organization, List<Guid> collectionIds)
{
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id)
.Returns(new Organization
{
PlanType = PlanType.EnterpriseAnnually,
MaxStorageGb = 100
});
var cipherInfos = ciphers.Select(c => (c,
string.IsNullOrEmpty(revisionDateString) ? null : (DateTime?)c.RevisionDate));
var sharingUserId = ciphers.First().UserId.Value;
await sutProvider.Sut.ShareManyAsync(cipherInfos, organization.Id, collectionIds, sharingUserId);
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync_vNext(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}
[Theory]
[BitAutoData]
public async Task RestoreAsync_UpdatesUserCipher(Guid restoringUserId, CipherDetails cipher, SutProvider<CipherService> sutProvider)
@@ -1093,6 +1120,33 @@ public class CipherServiceTests
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}
[Theory, BitAutoData]
public async Task ShareManyAsync_PaidOrgWithAttachment_WithBulkResourceCreationServiceEnabled_Passes(SutProvider<CipherService> sutProvider,
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
{
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
PlanType = PlanType.EnterpriseAnnually,
MaxStorageGb = 100
});
ciphers.FirstOrDefault().Attachments =
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";
var cipherInfos = ciphers.Select(c => (c,
(DateTime?)c.RevisionDate));
var sharingUserId = ciphers.First().UserId.Value;
await sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId);
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync_vNext(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}
private class SaveDetailsAsyncDependencies
{
public CipherDetails CipherDetails { get; set; }