1
0
mirror of https://github.com/bitwarden/server synced 2026-01-19 00:43:47 +00:00

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

This commit is contained in:
Daniel James Smith
2025-09-26 12:16:17 +02:00
committed by GitHub
112 changed files with 3404 additions and 1730 deletions

View File

@@ -10,6 +10,7 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
@@ -471,18 +472,32 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
var policyDetails = new PolicyDetails
{
OrganizationId = organization.Id,
OrganizationUserId = orgUser.Id,
IsProvider = false,
OrganizationUserStatus = orgUser.Status,
OrganizationUserType = orgUser.Type,
PolicyType = PolicyType.OrganizationDataOwnership
};
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organization.Id)
.Returns(new List<Guid> { orgUser.Id });
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails]));
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
await sutProvider.GetDependency<ICollectionRepository>()
.Received(1)
.UpsertDefaultCollectionsAsync(
organization.Id,
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.Id)),
collectionName);
.CreateAsync(
Arg.Is<Collection>(c =>
c.Name == collectionName &&
c.OrganizationId == organization.Id &&
c.Type == CollectionType.DefaultUserCollection),
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cu =>
cu.Single().Id == orgUser.Id &&
cu.Single().Manage));
}
[Theory, BitAutoData]
@@ -511,7 +526,7 @@ public class ConfirmOrganizationUserCommandTests
[Theory, BitAutoData]
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(
Organization org, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
[OrganizationUser(OrganizationUserStatusType.Accepted, OrganizationUserType.Owner)] OrganizationUser orgUser, User user,
string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
{
org.PlanType = PlanType.EnterpriseAnnually;
@@ -523,9 +538,18 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
var policyDetails = new PolicyDetails
{
OrganizationId = org.Id,
OrganizationUserId = orgUser.Id,
IsProvider = false,
OrganizationUserStatus = orgUser.Status,
OrganizationUserType = orgUser.Type,
PolicyType = PolicyType.OrganizationDataOwnership
};
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(org.Id)
.Returns(new List<Guid> { orgUser.UserId!.Value });
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, [policyDetails]));
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);

View File

@@ -1,4 +1,4 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -17,12 +17,12 @@ using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
[SutProviderCustomize]
public class DeleteClaimedOrganizationUserAccountCommandvNextTests
public class DeleteClaimedOrganizationUserAccountCommandTests
{
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithValidSingleUser_CallsDeleteManyUsersAsync(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -65,7 +65,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithEmptyUserIds_ReturnsEmptyResults(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
Guid organizationId,
Guid deletingUserId)
{
@@ -77,7 +77,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user1,
User user2,
Guid organizationId,
@@ -135,7 +135,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithValidationErrors_ReturnsErrorResults(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
Guid organizationId,
Guid orgUserId1,
Guid orgUserId2,
@@ -183,7 +183,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithMixedValidationResults_HandlesPartialSuccessCorrectly(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User validUser,
Guid organizationId,
Guid validOrgUserId,
@@ -243,7 +243,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_CancelPremiumsAsync_HandlesGatewayExceptionAndLogsWarning(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -285,7 +285,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
await sutProvider.GetDependency<IUserService>().Received(1).CancelPremiumAsync(user);
await AssertSuccessfulUserOperations(sutProvider, [user], [orgUser]);
sutProvider.GetDependency<ILogger<DeleteClaimedOrganizationUserAccountCommandvNext>>()
sutProvider.GetDependency<ILogger<DeleteClaimedOrganizationUserAccountCommand>>()
.Received(1)
.Log(
LogLevel.Warning,
@@ -299,7 +299,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
[Theory]
[BitAutoData]
public async Task CreateInternalRequests_CreatesCorrectRequestsForAllUsers(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user1,
User user2,
Guid organizationId,
@@ -326,7 +326,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(claimedStatuses);
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(callInfo =>
{
@@ -338,7 +338,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
await sutProvider.Sut.DeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId);
// Assert
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
.Received(1)
.ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>
requests.Count() == 2 &&
@@ -359,7 +359,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
[Theory]
[BitAutoData]
public async Task GetUsersAsync_WithNullUserIds_ReturnsEmptyCollection(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUserWithoutUserId)
@@ -374,7 +374,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()))
.Returns([]);
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(callInfo =>
{
@@ -386,7 +386,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserWithoutUserId.Id], deletingUserId);
// Assert
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
.Received(1)
.ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>
requests.Count() == 1 &&
@@ -406,7 +406,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
ValidationResultHelpers.Invalid(request, error);
private static void SetupRepositoryMocks(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
ICollection<OrganizationUser> orgUsers,
IEnumerable<User> users,
Guid organizationId,
@@ -426,16 +426,16 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
}
private static void SetupValidatorMock(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
IEnumerable<ValidationResult<DeleteUserValidationRequest>> validationResults)
{
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(validationResults);
}
private static async Task AssertSuccessfulUserOperations(
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
IEnumerable<User> expectedUsers,
IEnumerable<OrganizationUser> expectedOrgUsers)
{
@@ -457,7 +457,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests
events.Any(e => e.Item1.Id == expectedOrgUser.Id && e.Item2 == EventType.OrganizationUser_Deleted))));
}
private static async Task AssertNoUserOperations(SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider)
private static async Task AssertNoUserOperations(SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider)
{
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().DeleteManyAsync(default);
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushLogOutAsync(default);

View File

@@ -1,4 +1,4 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
@@ -13,12 +13,12 @@ using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
[SutProviderCustomize]
public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
public class DeleteClaimedOrganizationUserAccountValidatorTests
{
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithValidSingleRequest_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -50,7 +50,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithMultipleValidRequests_ReturnsAllValidResults(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user1,
User user2,
Guid organizationId,
@@ -97,7 +97,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithNullUser_ReturnsUserNotFoundError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
@@ -123,7 +123,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId)
@@ -149,7 +149,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithInvitedUser_ReturnsInvalidUserStatusError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -178,7 +178,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_WhenDeletingYourself_ReturnsCannotDeleteYourselfError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
[OrganizationUser] OrganizationUser organizationUser)
@@ -206,7 +206,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithUnclaimedUser_ReturnsUserNotClaimedError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -235,7 +235,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsNotOwner_ReturnsCannotDeleteOwnersError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -266,7 +266,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsOwner_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -296,7 +296,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithSoleOwnerOfOrganization_ReturnsSoleOwnerError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -331,7 +331,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithSoleProviderOwner_ReturnsSoleProviderError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -366,7 +366,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_CustomUserDeletingAdmin_ReturnsCannotDeleteAdminsError(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -397,7 +397,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_AdminDeletingAdmin_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
@@ -427,7 +427,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithMixedValidAndInvalidRequests_ReturnsCorrespondingResults(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User validUser,
User invalidUser,
Guid organizationId,
@@ -475,7 +475,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests
}
private static void SetupMocks(
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> sutProvider,
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
Guid organizationId,
Guid userId,
OrganizationUserType currentUserType = OrganizationUserType.Owner)

View File

@@ -1,526 +0,0 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
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 NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
[SutProviderCustomize]
public class DeleteClaimedOrganizationUserAccountCommandTests
{
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithValidUser_DeletesUserAndLogsEvent(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user, Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser)
{
// Arrange
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IUserRepository>()
.GetByIdAsync(user.Id)
.Returns(user);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)),
includeProvider: Arg.Any<bool>())
.Returns(true);
// Act
await sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId);
// Assert
await sutProvider.GetDependency<IUserService>().Received(1).DeleteAsync(user);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Deleted);
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithUserNotFound_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
Guid organizationId, Guid organizationUserId)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId)
.Returns((OrganizationUser?)null);
// Act
var exception = await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationId, organizationUserId, null));
// Assert
Assert.Equal("Member not found.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_DeletingYourself_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser,
Guid deletingUserId)
{
// Arrange
organizationUser.UserId = user.Id = deletingUserId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
.Returns(user);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
// Assert
Assert.Equal("You cannot delete yourself.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WhenUserIsInvited_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser organizationUser)
{
// Arrange
organizationUser.UserId = null;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, null));
// Assert
Assert.Equal("You cannot delete a member with Invited status.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WhenCustomUserDeletesAdmin_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser,
Guid deletingUserId)
{
// Arrange
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
.Returns(user);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationCustom(organizationUser.OrganizationId)
.Returns(true);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
// Assert
Assert.Equal("Custom users can not delete admins.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_DeletingOwnerWhenNotOwner_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
Guid deletingUserId)
{
// Arrange
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
.Returns(user);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationUser.OrganizationId)
.Returns(false);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
// Assert
Assert.Equal("Only owners can delete other owners.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_DeletingLastConfirmedOwner_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
Guid deletingUserId)
{
// Arrange
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
.Returns(user);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)),
includeProvider: Arg.Any<bool>())
.Returns(false);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
// Assert
Assert.Equal("Organization must have at least one confirmed owner.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithUserNotManaged_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser)
{
// Arrange
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
.Returns(user);
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, false } });
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, null));
// Assert
Assert.Equal("Member is not claimed by the organization.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user1, User user2, Guid organizationId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)
{
// Arrange
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
orgUser1.UserId = user1.Id;
orgUser2.UserId = user2.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser1, orgUser2 });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id)))
.Returns(new[] { user1, user2 });
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, true } });
// Act
var userIds = new[] { orgUser1.Id, orgUser2.Id };
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, userIds, null);
// Assert
Assert.Equal(2, results.Count());
Assert.All(results, r => Assert.Empty(r.Item2));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyAsync(userIds);
await sutProvider.GetDependency<IUserRepository>().Received(1).DeleteManyAsync(Arg.Is<IEnumerable<User>>(users => users.Any(u => u.Id == user1.Id) && users.Any(u => u.Id == user2.Id)));
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(
Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1
&& events.Count(e => e.Item1.Id == orgUser2.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1));
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenUserNotFound_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
Guid organizationId,
Guid orgUserId)
{
// Act
var result = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUserId }, null);
// Assert
Assert.Single(result);
Assert.Equal(orgUserId, result.First().Item1);
Assert.Contains("Member not found.", result.First().Item2);
await sutProvider.GetDependency<IUserRepository>()
.DidNotReceiveWithAnyArgs()
.DeleteManyAsync(default);
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenDeletingYourself_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user, [OrganizationUser] OrganizationUser orgUser, Guid deletingUserId)
{
// Arrange
orgUser.UserId = user.Id = deletingUserId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
.Returns(new[] { user });
// Act
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
// Assert
Assert.Single(result);
Assert.Equal(orgUser.Id, result.First().Item1);
Assert.Contains("You cannot delete yourself.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenUserIsInvited_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser orgUser)
{
// Arrange
orgUser.UserId = null;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser });
// Act
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, null);
// Assert
Assert.Single(result);
Assert.Equal(orgUser.Id, result.First().Item1);
Assert.Contains("You cannot delete a member with Invited status.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenDeletingOwnerAsNonOwner_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
Guid deletingUserId)
{
// Arrange
orgUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(user.Id)))
.Returns(new[] { user });
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgUser.OrganizationId)
.Returns(false);
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
Assert.Single(result);
Assert.Equal(orgUser.Id, result.First().Item1);
Assert.Contains("Only owners can delete other owners.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenDeletingLastOwner_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
Guid deletingUserId)
{
// Arrange
orgUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(user.Id)))
.Returns(new[] { user });
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any<IEnumerable<Guid>>(), Arg.Any<bool>())
.Returns(false);
// Act
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
// Assert
Assert.Single(result);
Assert.Equal(orgUser.Id, result.First().Item1);
Assert.Contains("Organization must have at least one confirmed owner.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenUserNotManaged_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
{
// Arrange
orgUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.UserId.Value)))
.Returns(new[] { user });
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, bool> { { orgUser.Id, false } });
// Act
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, null);
// Assert
Assert.Single(result);
Assert.Equal(orgUser.Id, result.First().Item1);
Assert.Contains("Member is not claimed by the organization.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_MixedValidAndInvalidUsers_ReturnsAppropriateResults(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user1, User user3,
Guid organizationId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser orgUser2,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser3)
{
// Arrange
orgUser1.UserId = user1.Id;
orgUser2.UserId = null;
orgUser3.UserId = user3.Id;
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser1, orgUser2, orgUser3 });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user3.Id)))
.Returns(new[] { user1, user3 });
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser3.Id, false } });
// Act
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUser1.Id, orgUser2.Id, orgUser3.Id }, null);
// Assert
Assert.Equal(3, results.Count());
Assert.Empty(results.First(r => r.Item1 == orgUser1.Id).Item2);
Assert.Equal("You cannot delete a member with Invited status.", results.First(r => r.Item1 == orgUser2.Id).Item2);
Assert.Equal("Member is not claimed by the organization.", results.First(r => r.Item1 == orgUser3.Id).Item2);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(
Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1));
}
}

View File

@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Organizations.Queries;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
@@ -13,11 +14,14 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Stripe.Tax;
using Stripe.TestHelpers;
using Xunit;
namespace Bit.Core.Test.Billing.Organizations.Queries;
using static StripeConstants;
[SutProviderCustomize]
public class GetOrganizationWarningsQueryTests
{
@@ -57,7 +61,7 @@ public class GetOrganizationWarningsQueryTests
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Trialing,
Status = SubscriptionStatus.Trialing,
TrialEnd = now.AddDays(7),
Customer = new Customer
{
@@ -95,7 +99,7 @@ public class GetOrganizationWarningsQueryTests
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Trialing,
Status = SubscriptionStatus.Trialing,
TrialEnd = now.AddDays(7),
Customer = new Customer
{
@@ -142,7 +146,7 @@ public class GetOrganizationWarningsQueryTests
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Unpaid,
Status = SubscriptionStatus.Unpaid,
Customer = new Customer
{
InvoiceSettings = new CustomerInvoiceSettings(),
@@ -170,7 +174,8 @@ public class GetOrganizationWarningsQueryTests
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Unpaid
Customer = new Customer(),
Status = SubscriptionStatus.Unpaid
});
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)
@@ -197,7 +202,8 @@ public class GetOrganizationWarningsQueryTests
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Unpaid
Customer = new Customer(),
Status = SubscriptionStatus.Unpaid
});
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
@@ -223,7 +229,8 @@ public class GetOrganizationWarningsQueryTests
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Canceled
Customer = new Customer(),
Status = SubscriptionStatus.Canceled
});
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
@@ -249,7 +256,8 @@ public class GetOrganizationWarningsQueryTests
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Unpaid
Customer = new Customer(),
Status = SubscriptionStatus.Unpaid
});
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(false);
@@ -275,8 +283,9 @@ public class GetOrganizationWarningsQueryTests
))
.Returns(new Subscription
{
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
Status = StripeConstants.SubscriptionStatus.Active,
CollectionMethod = CollectionMethod.SendInvoice,
Customer = new Customer(),
Status = SubscriptionStatus.Active,
CurrentPeriodEnd = now.AddDays(10),
TestClock = new TestClock
{
@@ -313,11 +322,12 @@ public class GetOrganizationWarningsQueryTests
))
.Returns(new Subscription
{
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
Status = StripeConstants.SubscriptionStatus.Active,
CollectionMethod = CollectionMethod.SendInvoice,
Customer = new Customer(),
Status = SubscriptionStatus.Active,
LatestInvoice = new Invoice
{
Status = StripeConstants.InvoiceStatus.Open,
Status = InvoiceStatus.Open,
DueDate = now.AddDays(30),
Created = now
},
@@ -360,8 +370,9 @@ public class GetOrganizationWarningsQueryTests
.Returns(new Subscription
{
Id = subscriptionId,
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
Status = StripeConstants.SubscriptionStatus.PastDue,
CollectionMethod = CollectionMethod.SendInvoice,
Customer = new Customer(),
Status = SubscriptionStatus.PastDue,
TestClock = new TestClock
{
FrozenTime = now
@@ -390,4 +401,406 @@ public class GetOrganizationWarningsQueryTests
Assert.Equal(dueDate.AddDays(30), response.ResellerRenewal.PastDue!.SuspensionDate);
}
[Theory, BitAutoData]
public async Task Run_USCustomer_NoTaxIdWarning(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
var subscription = new Subscription
{
Customer = new Customer
{
Address = new Address { Country = "US" },
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
var response = await sutProvider.Sut.Run(organization);
Assert.Null(response.TaxId);
}
[Theory, BitAutoData]
public async Task Run_FreeCustomer_NoTaxIdWarning(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
organization.PlanType = PlanType.Free;
var subscription = new Subscription
{
Customer = new Customer
{
Address = new Address { Country = "CA" },
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
var response = await sutProvider.Sut.Run(organization);
Assert.Null(response.TaxId);
}
[Theory, BitAutoData]
public async Task Run_NotOwner_NoTaxIdWarning(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
organization.PlanType = PlanType.TeamsAnnually;
var subscription = new Subscription
{
Customer = new Customer
{
Address = new Address { Country = "CA" },
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organization.Id)
.Returns(false);
var response = await sutProvider.Sut.Run(organization);
Assert.Null(response.TaxId);
}
[Theory, BitAutoData]
public async Task Run_HasProvider_NoTaxIdWarning(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
organization.PlanType = PlanType.TeamsAnnually;
var subscription = new Subscription
{
Customer = new Customer
{
Address = new Address { Country = "CA" },
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organization.Id)
.Returns(true);
sutProvider.GetDependency<IProviderRepository>()
.GetByOrganizationIdAsync(organization.Id)
.Returns(new Provider());
var response = await sutProvider.Sut.Run(organization);
Assert.Null(response.TaxId);
}
[Theory, BitAutoData]
public async Task Run_NoRegistrationInCountry_NoTaxIdWarning(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
organization.PlanType = PlanType.TeamsAnnually;
var subscription = new Subscription
{
Customer = new Customer
{
Address = new Address { Country = "CA" },
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organization.Id)
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
{
new() { Country = "GB" }
}
});
var response = await sutProvider.Sut.Run(organization);
Assert.Null(response.TaxId);
}
[Theory, BitAutoData]
public async Task Run_Has_TaxIdWarning_Missing(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
organization.PlanType = PlanType.TeamsAnnually;
var subscription = new Subscription
{
Customer = new Customer
{
Address = new Address { Country = "CA" },
TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organization.Id)
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
{
new() { Country = "CA" }
}
});
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
TaxId.Type: "tax_id_missing"
});
}
[Theory, BitAutoData]
public async Task Run_Has_TaxIdWarning_PendingVerification(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
organization.PlanType = PlanType.EnterpriseAnnually;
var taxId = new TaxId
{
Verification = new TaxIdVerification
{
Status = TaxIdVerificationStatus.Pending
}
};
var subscription = new Subscription
{
Customer = new Customer
{
Address = new Address { Country = "CA" },
TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organization.Id)
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
{
new() { Country = "CA" }
}
});
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
TaxId.Type: "tax_id_pending_verification"
});
}
[Theory, BitAutoData]
public async Task Run_Has_TaxIdWarning_FailedVerification(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
organization.PlanType = PlanType.TeamsAnnually;
var taxId = new TaxId
{
Verification = new TaxIdVerification
{
Status = TaxIdVerificationStatus.Unverified
}
};
var subscription = new Subscription
{
Customer = new Customer
{
Address = new Address { Country = "CA" },
TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organization.Id)
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
{
new() { Country = "CA" }
}
});
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
TaxId.Type: "tax_id_failed_verification"
});
}
[Theory, BitAutoData]
public async Task Run_VerifiedTaxId_NoTaxIdWarning(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
organization.PlanType = PlanType.TeamsAnnually;
var taxId = new TaxId
{
Verification = new TaxIdVerification
{
Status = TaxIdVerificationStatus.Verified
}
};
var subscription = new Subscription
{
Customer = new Customer
{
Address = new Address { Country = "CA" },
TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organization.Id)
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
{
new() { Country = "CA" }
}
});
var response = await sutProvider.Sut.Run(organization);
Assert.Null(response.TaxId);
}
[Theory, BitAutoData]
public async Task Run_NullVerification_NoTaxIdWarning(
Organization organization,
SutProvider<GetOrganizationWarningsQuery> sutProvider)
{
organization.PlanType = PlanType.TeamsAnnually;
var taxId = new TaxId
{
Verification = null
};
var subscription = new Subscription
{
Customer = new Customer
{
Address = new Address { Country = "CA" },
TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organization.Id)
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
{
new() { Country = "CA" }
}
});
var response = await sutProvider.Sut.Run(organization);
Assert.Null(response.TaxId);
}
}

View File

@@ -0,0 +1,322 @@
#nullable enable
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Kdf.Implementations;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.KeyManagement.Kdf;
[SutProviderCustomize]
public class ChangeKdfCommandTests
{
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_ChangesKdfAsync(SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(IdentityResult.Success));
var kdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 4,
Memory = 512,
Parallelism = 4
};
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "newMasterPassword",
Salt = user.GetMasterPasswordSalt()
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = kdf,
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey",
Salt = user.GetMasterPasswordSalt()
};
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u =>
u.Id == user.Id
&& u.Kdf == Enums.KdfType.Argon2id
&& u.KdfIterations == 4
&& u.KdfMemory == 512
&& u.KdfParallelism == 4
));
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_UserIsNull_ThrowsArgumentNullException(SutProvider<ChangeKdfCommand> sutProvider)
{
var kdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 4,
Memory = 512,
Parallelism = 4
};
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "newMasterPassword",
Salt = "salt"
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = kdf,
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey",
Salt = "salt"
};
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
await sutProvider.Sut.ChangeKdfAsync(null!, "masterPassword", authenticationData, unlockData));
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(false));
var kdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 4,
Memory = 512,
Parallelism = 4
};
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "newMasterPassword",
Salt = user.GetMasterPasswordSalt()
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = kdf,
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey",
Salt = user.GetMasterPasswordSalt()
};
var result = await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
Assert.False(result.Succeeded);
Assert.Contains(result.Errors, e => e.Code == "PasswordMismatch");
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_WithAuthenticationAndUnlockData_UpdatesUserCorrectly(SutProvider<ChangeKdfCommand> sutProvider, User user)
{
var constantKdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 5,
Memory = 1024,
Parallelism = 4
};
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = constantKdf,
MasterPasswordAuthenticationHash = "new-auth-hash",
Salt = user.GetMasterPasswordSalt()
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = constantKdf,
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = user.GetMasterPasswordSalt()
};
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(IdentityResult.Success));
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u =>
u.Id == user.Id
&& u.Kdf == constantKdf.KdfType
&& u.KdfIterations == constantKdf.Iterations
&& u.KdfMemory == constantKdf.Memory
&& u.KdfParallelism == constantKdf.Parallelism
&& u.Key == "new-wrapped-key"
));
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 },
MasterPasswordAuthenticationHash = "new-auth-hash",
Salt = user.GetMasterPasswordSalt()
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = new KdfSettings { KdfType = Enums.KdfType.PBKDF2_SHA256, Iterations = 100000 },
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = user.GetMasterPasswordSalt()
};
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, KdfSettings kdf)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "new-auth-hash",
Salt = "different-salt"
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = kdf,
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = user.GetMasterPasswordSalt()
};
await Assert.ThrowsAsync<ArgumentException>(async () =>
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, KdfSettings kdf)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "new-auth-hash",
Salt = user.GetMasterPasswordSalt()
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = kdf,
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = "different-salt"
};
await Assert.ThrowsAsync<ArgumentException>(async () =>
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
var failedResult = IdentityResult.Failed(new IdentityError { Code = "TestFail", Description = "Test fail" });
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(failedResult));
var kdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 4,
Memory = 512,
Parallelism = 4
};
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
MasterPasswordAuthenticationHash = "newMasterPassword",
Salt = user.GetMasterPasswordSalt()
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = kdf,
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey",
Salt = user.GetMasterPasswordSalt()
};
var result = await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
Assert.False(result.Succeeded);
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
// Create invalid KDF settings (iterations too low for PBKDF2)
var invalidKdf = new KdfSettings
{
KdfType = Enums.KdfType.PBKDF2_SHA256,
Iterations = 1000, // This is below the minimum of 600,000
Memory = null,
Parallelism = null
};
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = invalidKdf,
MasterPasswordAuthenticationHash = "new-auth-hash",
Salt = user.GetMasterPasswordSalt()
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = invalidKdf,
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = user.GetMasterPasswordSalt()
};
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
Assert.Equal("KDF settings are invalid.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
// Create invalid Argon2 KDF settings (memory too high)
var invalidKdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 3, // Valid
Memory = 2048, // This is above the maximum of 1024
Parallelism = 4 // Valid
};
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = invalidKdf,
MasterPasswordAuthenticationHash = "new-auth-hash",
Salt = user.GetMasterPasswordSalt()
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = invalidKdf,
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = user.GetMasterPasswordSalt()
};
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
Assert.Equal("KDF settings are invalid.", exception.Message);
}
}

View File

@@ -0,0 +1,230 @@
using System.Security.Cryptography;
using System.Text;
using Bit.Core.Utilities;
using Xunit;
namespace Bit.Core.Test.Utilities;
public class EnumerationProtectionHelpersTests
{
#region GetIndexForInputHash Tests
[Fact]
public void GetIndexForInputHash_NullHmacKey_ReturnsZero()
{
// Arrange
byte[] hmacKey = null;
var salt = "test@example.com";
var range = 10;
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void GetIndexForInputHash_ZeroRange_ReturnsZero()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "test@example.com";
var range = 0;
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void GetIndexForInputHash_NegativeRange_ReturnsZero()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "test@example.com";
var range = -5;
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void GetIndexForInputHash_ValidInputs_ReturnsConsistentResult()
{
// Arrange
var hmacKey = Encoding.UTF8.GetBytes("test-key-12345678901234567890123456789012");
var salt = "test@example.com";
var range = 10;
// Act
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(result1, result2);
Assert.InRange(result1, 0, range - 1);
}
[Fact]
public void GetIndexForInputHash_SameInputSameKey_AlwaysReturnsSameResult()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "consistent@example.com";
var range = 100;
// Act - Call multiple times
var results = new int[10];
for (var i = 0; i < 10; i++)
{
results[i] = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
}
// Assert - All results should be identical
Assert.All(results, result => Assert.Equal(results[0], result));
Assert.All(results, result => Assert.InRange(result, 0, range - 1));
}
[Fact]
public void GetIndexForInputHash_DifferentInputsSameKey_ReturnsDifferentResults()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt1 = "user1@example.com";
var salt2 = "user2@example.com";
var range = 100;
// Act
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt1, range);
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt2, range);
// Assert
Assert.NotEqual(result1, result2);
Assert.InRange(result1, 0, range - 1);
Assert.InRange(result2, 0, range - 1);
}
[Fact]
public void GetIndexForInputHash_DifferentKeysSameInput_ReturnsDifferentResults()
{
// Arrange
var hmacKey1 = RandomNumberGenerator.GetBytes(32);
var hmacKey2 = RandomNumberGenerator.GetBytes(32);
var salt = "test@example.com";
var range = 100;
// Act
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey1, salt, range);
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey2, salt, range);
// Assert
Assert.NotEqual(result1, result2);
Assert.InRange(result1, 0, range - 1);
Assert.InRange(result2, 0, range - 1);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(5)]
[InlineData(10)]
[InlineData(100)]
[InlineData(1000)]
public void GetIndexForInputHash_VariousRanges_ReturnsValidIndex(int range)
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "test@example.com";
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.InRange(result, 0, range - 1);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void GetIndexForInputHash_EmptyString_HandlesGracefully(string salt)
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, 10);
// Assert
Assert.InRange(result, 0, 9);
}
[Fact]
public void GetIndexForInputHash_NullInput_ThrowsException()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
string salt = null;
var range = 10;
// Act & Assert
Assert.Throws<NullReferenceException>(() =>
EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range));
}
[Fact]
public void GetIndexForInputHash_SpecialCharacters_HandlesCorrectly()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "test+user@example.com!@#$%^&*()";
var range = 50;
// Act
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(result1, result2);
Assert.InRange(result1, 0, range - 1);
}
[Fact]
public void GetIndexForInputHash_UnicodeCharacters_HandlesCorrectly()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = "tëst@éxämplé.cöm";
var range = 25;
// Act
var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.Equal(result1, result2);
Assert.InRange(result1, 0, range - 1);
}
[Fact]
public void GetIndexForInputHash_LongInput_HandlesCorrectly()
{
// Arrange
var hmacKey = RandomNumberGenerator.GetBytes(32);
var salt = new string('a', 1000) + "@example.com";
var range = 30;
// Act
var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);
// Assert
Assert.InRange(result, 0, range - 1);
}
#endregion
}