1
0
mirror of https://github.com/bitwarden/server synced 2026-01-16 07:23:15 +00:00

Merge branch 'main' of github.com:bitwarden/server into arch/seeder-api

This commit is contained in:
Hinton
2026-01-08 11:15:13 +01:00
500 changed files with 54684 additions and 3482 deletions

View File

@@ -96,4 +96,23 @@ public class OrganizationTests
var host = Assert.Contains("Host", (IDictionary<string, object>)duo.MetaData);
Assert.Equal("Host_value", host);
}
[Fact]
public void UseDisableSmAdsForUsers_DefaultValue_IsFalse()
{
var organization = new Organization();
Assert.False(organization.UseDisableSmAdsForUsers);
}
[Fact]
public void UseDisableSmAdsForUsers_CanBeSetToTrue()
{
var organization = new Organization
{
UseDisableSmAdsForUsers = true
};
Assert.True(organization.UseDisableSmAdsForUsers);
}
}

View File

@@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Utilities.v2;
@@ -727,4 +728,54 @@ public class AutomaticallyConfirmUsersCommandTests
Arg.Any<IEnumerable<string>>(),
organization.Id.ToString());
}
[Theory]
[BitAutoData]
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOn_UsesNewMailer(
Organization organization,
string userEmail,
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider)
{
// Arrange
const bool accessSecretsManager = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
.Returns(true);
// Act
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(organization, userEmail, accessSecretsManager);
// Assert
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
.Received(1)
.SendConfirmationAsync(organization, userEmail, accessSecretsManager);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOrganizationConfirmedEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>());
}
[Theory]
[BitAutoData]
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOff_UsesLegacyMailService(
Organization organization,
string userEmail,
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider)
{
// Arrange
const bool accessSecretsManager = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
.Returns(false);
// Act
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(organization, userEmail, accessSecretsManager);
// Assert
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationConfirmedEmailAsync(organization.Name, userEmail, accessSecretsManager);
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
.DidNotReceive()
.SendConfirmationAsync(Arg.Any<Organization>(), Arg.Any<string>(), Arg.Any<bool>());
}
}

View File

@@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
@@ -462,7 +463,7 @@ public class ConfirmOrganizationUserCommandTests
}
[Theory, BitAutoData]
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection(
public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection(
Organization organization, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
@@ -475,8 +476,6 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
var policyDetails = new PolicyDetails
{
OrganizationId = organization.Id,
@@ -506,7 +505,7 @@ public class ConfirmOrganizationUserCommandTests
}
[Theory, BitAutoData]
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection(
public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection(
Organization org, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
@@ -519,8 +518,6 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, "");
await sutProvider.GetDependency<ICollectionRepository>()
@@ -529,7 +526,7 @@ public class ConfirmOrganizationUserCommandTests
}
[Theory, BitAutoData]
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(
public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(
Organization org, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted, OrganizationUserType.Owner)] OrganizationUser orgUser, User user,
string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
@@ -541,7 +538,6 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
var policyDetails = new PolicyDetails
{
@@ -814,4 +810,52 @@ public class ConfirmOrganizationUserCommandTests
Assert.Empty(result[1].Item2);
Assert.Equal(new OtherOrganizationDoesNotAllowOtherMembership().Message, result[2].Item2);
}
[Theory, BitAutoData]
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOn_UsesNewMailer(
Organization org,
string userEmail,
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
{
// Arrange
const bool accessSecretsManager = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
.Returns(true);
// Act
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(org, userEmail, accessSecretsManager);
// Assert - verify new mailer is called, not legacy mail service
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
.Received(1)
.SendConfirmationAsync(org, userEmail, accessSecretsManager);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOrganizationConfirmedEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>());
}
[Theory, BitAutoData]
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOff_UsesLegacyMailService(
Organization org,
string userEmail,
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
{
// Arrange
const bool accessSecretsManager = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
.Returns(false);
// Act
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(org, userEmail, accessSecretsManager);
// Assert
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationConfirmedEmailAsync(org.DisplayName(), userEmail, accessSecretsManager);
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
.DidNotReceive()
.SendConfirmationAsync(Arg.Any<Organization>(), Arg.Any<string>(), Arg.Any<bool>());
}
}

View File

@@ -0,0 +1,245 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
using Bit.Core.Billing.Enums;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
[SutProviderCustomize]
public class SendOrganizationConfirmationCommandTests
{
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendConfirmationAsync_EnterpriseOrganization_SendsEnterpriseTeamsEmail(
Organization organization,
string userEmail,
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
{
// Arrange
organization.PlanType = PlanType.EnterpriseAnnually;
organization.Name = "Test Enterprise Org";
// Act
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
// Assert
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
mail.ToEmails.Contains(userEmail) &&
mail.ToEmails.Count() == 1 &&
mail.View.OrganizationName == organization.Name &&
mail.Subject == GetSubject(organization.Name)));
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendConfirmationAsync_TeamsOrganization_SendsEnterpriseTeamsEmail(
Organization organization,
string userEmail,
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
{
// Arrange
organization.PlanType = PlanType.TeamsAnnually;
organization.Name = "Test Teams Org";
// Act
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
// Assert
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
mail.ToEmails.Contains(userEmail) &&
mail.ToEmails.Count() == 1 &&
mail.View.OrganizationName == organization.Name &&
mail.Subject == GetSubject(organization.Name)));
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendConfirmationAsync_FamilyOrganization_SendsFamilyFreeEmail(
Organization organization,
string userEmail,
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
{
// Arrange
organization.PlanType = PlanType.FamiliesAnnually;
organization.Name = "Test Family Org";
// Act
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
// Assert
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Is<OrganizationConfirmationFamilyFree>(mail =>
mail.ToEmails.Contains(userEmail) &&
mail.ToEmails.Count() == 1 &&
mail.View.OrganizationName == organization.Name &&
mail.Subject == GetSubject(organization.Name)));
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendConfirmationAsync_FreeOrganization_SendsFamilyFreeEmail(
Organization organization,
string userEmail,
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
{
// Arrange
organization.PlanType = PlanType.Free;
organization.Name = "Test Free Org";
// Act
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
// Assert
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Is<OrganizationConfirmationFamilyFree>(mail =>
mail.ToEmails.Contains(userEmail) &&
mail.ToEmails.Count() == 1 &&
mail.View.OrganizationName == organization.Name &&
mail.Subject == GetSubject(organization.Name)));
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendConfirmationsAsync_MultipleUsers_SendsSingleEmail(
Organization organization,
List<string> userEmails,
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
{
// Arrange
organization.PlanType = PlanType.EnterpriseAnnually;
organization.Name = "Test Enterprise Org";
// Act
await sutProvider.Sut.SendConfirmationsAsync(organization, userEmails, false);
// Assert
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
mail.ToEmails.SequenceEqual(userEmails) &&
mail.View.OrganizationName == organization.Name));
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendConfirmationsAsync_EmptyUserList_DoesNotSendEmail(
Organization organization,
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
{
// Arrange
organization.PlanType = PlanType.EnterpriseAnnually;
organization.Name = "Test Enterprise Org";
// Act
await sutProvider.Sut.SendConfirmationsAsync(organization, [], false);
// Assert
await sutProvider.GetDependency<IMailer>().DidNotReceive()
.SendEmail(Arg.Any<OrganizationConfirmationEnterpriseTeams>());
await sutProvider.GetDependency<IMailer>().DidNotReceive()
.SendEmail(Arg.Any<OrganizationConfirmationFamilyFree>());
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendConfirmationAsync_HtmlEncodedOrganizationName_DecodesNameCorrectly(
Organization organization,
string userEmail,
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
{
// Arrange
organization.PlanType = PlanType.EnterpriseAnnually;
organization.Name = "Test &amp; Company";
var expectedDecodedName = "Test & Company";
// Act
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
// Assert
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
mail.View.OrganizationName == expectedDecodedName));
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendConfirmationAsync_AllEnterpriseTeamsPlanTypes_SendsEnterpriseTeamsEmail(
Organization organization,
string userEmail,
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
{
// Test all Enterprise and Teams plan types
var enterpriseTeamsPlanTypes = new[]
{
PlanType.TeamsMonthly2019, PlanType.TeamsAnnually2019,
PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020,
PlanType.TeamsMonthly2023, PlanType.TeamsAnnually2023,
PlanType.TeamsStarter2023, PlanType.TeamsMonthly,
PlanType.TeamsAnnually, PlanType.TeamsStarter,
PlanType.EnterpriseMonthly2019, PlanType.EnterpriseAnnually2019,
PlanType.EnterpriseMonthly2020, PlanType.EnterpriseAnnually2020,
PlanType.EnterpriseMonthly2023, PlanType.EnterpriseAnnually2023,
PlanType.EnterpriseMonthly, PlanType.EnterpriseAnnually
};
foreach (var planType in enterpriseTeamsPlanTypes)
{
// Arrange
organization.PlanType = planType;
organization.Name = "Test Org";
sutProvider.GetDependency<IMailer>().ClearReceivedCalls();
// Act
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
// Assert
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Any<OrganizationConfirmationEnterpriseTeams>());
await sutProvider.GetDependency<IMailer>().DidNotReceive()
.SendEmail(Arg.Any<OrganizationConfirmationFamilyFree>());
}
}
[Theory]
[OrganizationCustomize, BitAutoData]
public async Task SendConfirmationAsync_AllFamilyFreePlanTypes_SendsFamilyFreeEmail(
Organization organization,
string userEmail,
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
{
// Test all Family, Free, and Custom plan types
var familyFreePlanTypes = new[]
{
PlanType.Free, PlanType.FamiliesAnnually2019,
PlanType.FamiliesAnnually2025, PlanType.FamiliesAnnually,
PlanType.Custom
};
foreach (var planType in familyFreePlanTypes)
{
// Arrange
organization.PlanType = planType;
organization.Name = "Test Org";
sutProvider.GetDependency<IMailer>().ClearReceivedCalls();
// Act
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
// Assert
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Any<OrganizationConfirmationFamilyFree>());
await sutProvider.GetDependency<IMailer>().DidNotReceive()
.SendEmail(Arg.Any<OrganizationConfirmationEnterpriseTeams>());
}
}
private static string GetSubject(string organizationName) => $"You Have Been Confirmed To {organizationName}";
}

View File

@@ -0,0 +1,233 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Entities;
using Bit.Core.Enums;
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 NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;
[SutProviderCustomize]
public class SelfRevokeOrganizationUserCommandTests
{
[Theory]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task SelfRevokeUser_Success(
OrganizationUserType userType,
Guid organizationId,
Guid userId,
[OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser organizationUser,
SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)
{
// Arrange
organizationUser.Type = userType;
organizationUser.OrganizationId = organizationId;
organizationUser.UserId = userId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns(organizationUser);
// Create policy requirement with confirmed user
var policyDetails = new List<PolicyDetails>
{
new()
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
OrganizationUserType = userType,
PolicyType = PolicyType.OrganizationDataOwnership
}
};
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Enabled,
policyDetails);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
.Returns(policyRequirement);
// Act
var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);
// Assert
Assert.True(result.IsSuccess);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.RevokeAsync(organizationUser.Id);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_SelfRevoked);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushSyncOrgKeysAsync(userId);
}
[Theory, BitAutoData]
public async Task SelfRevokeUser_WhenUserNotFound_ReturnsNotFoundError(
Guid organizationId,
Guid userId,
SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns((OrganizationUser)null);
// Act
var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);
// Assert
Assert.True(result.IsError);
Assert.IsType<OrganizationUserNotFound>(result.AsError);
}
[Theory, BitAutoData]
public async Task SelfRevokeUser_WhenNotEligible_ReturnsBadRequestError(
Guid organizationId,
Guid userId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser,
SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)
{
// Arrange
organizationUser.OrganizationId = organizationId;
organizationUser.UserId = userId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns(organizationUser);
// Policy requirement with no policies (disabled)
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Disabled,
Enumerable.Empty<PolicyDetails>());
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
.Returns(policyRequirement);
// Act
var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);
// Assert
Assert.True(result.IsError);
Assert.IsType<NotEligibleForSelfRevoke>(result.AsError);
}
[Theory, BitAutoData]
public async Task SelfRevokeUser_WhenLastOwner_ReturnsBadRequestError(
Guid organizationId,
Guid userId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)
{
// Arrange
organizationUser.OrganizationId = organizationId;
organizationUser.UserId = userId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns(organizationUser);
// Create policy requirement with confirmed owner
var policyDetails = new List<PolicyDetails>
{
new()
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
OrganizationUserType = OrganizationUserType.Owner,
PolicyType = PolicyType.OrganizationDataOwnership
}
};
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Enabled,
policyDetails);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
.Returns(policyRequirement);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>(), true)
.Returns(false);
// Act
var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);
// Assert
Assert.True(result.IsError);
Assert.IsType<LastOwnerCannotSelfRevoke>(result.AsError);
}
[Theory, BitAutoData]
public async Task SelfRevokeUser_WhenOwnerButNotLastOwner_Success(
Guid organizationId,
Guid userId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)
{
// Arrange
organizationUser.OrganizationId = organizationId;
organizationUser.UserId = userId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns(organizationUser);
// Create policy requirement with confirmed owner
var policyDetails = new List<PolicyDetails>
{
new()
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
OrganizationUserType = OrganizationUserType.Owner,
PolicyType = PolicyType.OrganizationDataOwnership
}
};
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Enabled,
policyDetails);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
.Returns(policyRequirement);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>(), true)
.Returns(true);
// Act
var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);
// Assert
Assert.True(result.IsSuccess);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.RevokeAsync(organizationUser.Id);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_SelfRevoked);
}
}

View File

@@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -162,8 +163,9 @@ public class OrganizationUpdateCommandTests
OrganizationId = organizationId,
Name = organization.Name,
BillingEmail = organization.BillingEmail,
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
Keys = new PublicKeyEncryptionKeyPairData(
wrappedPrivateKey: encryptedPrivateKey,
publicKey: publicKey)
};
// Act
@@ -207,8 +209,9 @@ public class OrganizationUpdateCommandTests
OrganizationId = organizationId,
Name = organization.Name,
BillingEmail = organization.BillingEmail,
PublicKey = newPublicKey,
EncryptedPrivateKey = newEncryptedPrivateKey
Keys = new PublicKeyEncryptionKeyPairData(
wrappedPrivateKey: newEncryptedPrivateKey,
publicKey: newPublicKey)
};
// Act
@@ -394,8 +397,9 @@ public class OrganizationUpdateCommandTests
OrganizationId = organizationId,
Name = newName, // Should be ignored
BillingEmail = newBillingEmail, // Should be ignored
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
Keys = new PublicKeyEncryptionKeyPairData(
wrappedPrivateKey: encryptedPrivateKey,
publicKey: publicKey)
};
// Act

View File

@@ -36,6 +36,73 @@ public class OrganizationDataOwnershipPolicyRequirementFactoryTests
Assert.Equal(PolicyType.OrganizationDataOwnership, sutProvider.Sut.PolicyType);
}
[Theory, BitAutoData]
public void EligibleForSelfRevoke_WithConfirmedUser_ReturnsTrue(
Guid organizationId,
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
// Arrange
policies[0].OrganizationId = organizationId;
var requirement = sutProvider.Sut.Create(policies);
// Act
var result = requirement.EligibleForSelfRevoke(organizationId);
// Assert
Assert.True(result);
}
[Theory, BitAutoData]
public void EligibleForSelfRevoke_WithInvitedUser_ReturnsFalse(
Guid organizationId,
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Invited)] PolicyDetails[] policies,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
// Arrange
policies[0].OrganizationId = organizationId;
var requirement = sutProvider.Sut.Create(policies);
// Act
var result = requirement.EligibleForSelfRevoke(organizationId);
// Assert
Assert.False(result);
}
[Theory, BitAutoData]
public void EligibleForSelfRevoke_WithNoPolicies_ReturnsFalse(
Guid organizationId,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
// Arrange
var requirement = sutProvider.Sut.Create([]);
// Act
var result = requirement.EligibleForSelfRevoke(organizationId);
// Assert
Assert.False(result);
}
[Theory, BitAutoData]
public void EligibleForSelfRevoke_WithDifferentOrganization_ReturnsFalse(
Guid organizationId,
Guid differentOrganizationId,
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
// Arrange
policies[0].OrganizationId = differentOrganizationId;
var requirement = sutProvider.Sut.Create(policies);
// Act
var result = requirement.EligibleForSelfRevoke(organizationId);
// Assert
Assert.False(result);
}
[Theory, BitAutoData]
public void GetDefaultCollectionRequestOnPolicyEnable_WithConfirmedUser_ReturnsTrue(
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,

View File

@@ -6,7 +6,6 @@ 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;
@@ -20,29 +19,6 @@ 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, 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,
@@ -54,10 +30,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
@@ -80,10 +52,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
@@ -234,10 +202,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
policyUpdate.Enabled = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, metadata);
// Act
@@ -264,39 +228,10 @@ public class OrganizationDataOwnershipPolicyValidatorTests
IPolicyRepository policyRepository,
ICollectionRepository collectionRepository)
{
var featureService = Substitute.For<IFeatureService>();
featureService
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService);
var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory]);
return sut;
}
[Theory, BitAutoData]
public async Task ExecutePostUpsertSideEffectAsync_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, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task ExecutePostUpsertSideEffectAsync_PolicyAlreadyEnabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
@@ -308,10 +243,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
@@ -334,10 +265,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
@@ -432,10 +359,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
policyUpdate.Enabled = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, metadata);
// Act

View File

@@ -1,45 +0,0 @@
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
using Bit.Core.Services;
using Xunit;
namespace Bit.Core.Test.Services;
public class IntegrationHandlerTests
{
[Fact]
public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage()
{
var sut = new TestIntegrationHandler();
var expected = new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
{
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
MessageId = "TestMessageId",
OrganizationId = "TestOrganizationId",
IntegrationType = IntegrationType.Webhook,
RenderedTemplate = "Template",
DelayUntilDate = null,
RetryCount = 0
};
var result = await sut.HandleAsync(expected.ToJson());
var typedResult = Assert.IsType<IntegrationMessage<WebhookIntegrationConfigurationDetails>>(result.Message);
Assert.Equal(expected.MessageId, typedResult.MessageId);
Assert.Equal(expected.OrganizationId, typedResult.OrganizationId);
Assert.Equal(expected.Configuration, typedResult.Configuration);
Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate);
Assert.Equal(expected.IntegrationType, typedResult.IntegrationType);
}
private class TestIntegrationHandler : IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
{
public override Task<IntegrationHandlerResult> HandleAsync(
IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var result = new IntegrationHandlerResult(success: true, message: message);
return Task.FromResult(result);
}
}
}

View File

@@ -1,4 +1,4 @@
using Bit.Core.Enums;
using Bit.Core.Dirt.Enums;
using Xunit;
namespace Bit.Core.Test.Services;

View File

@@ -214,6 +214,7 @@ If you believe you need to change the version for a valid reason, please discuss
AllowAdminAccessToAllCollectionItems = true,
UseOrganizationDomains = true,
UseAdminSponsoredFamilies = false,
UseDisableSmAdsForUsers = false,
UsePhishingBlocker = false,
};
}
@@ -260,4 +261,34 @@ If you believe you need to change the version for a valid reason, please discuss
.Returns([0x00, 0x01, 0x02, 0x03]); // Dummy signature for hash testing
return mockService;
}
/// <summary>
/// Verifies that UseDisableSmAdsForUsers claim is properly generated in the license Token
/// and that VerifyData correctly validates the claim.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public void OrganizationLicense_UseDisableSmAdsForUsers_ClaimGenerationAndValidation(bool useDisableSmAdsForUsers, ClaimsPrincipal claimsPrincipal)
{
// Arrange
var organization = CreateDeterministicOrganization();
organization.UseDisableSmAdsForUsers = useDisableSmAdsForUsers;
var subscriptionInfo = CreateDeterministicSubscriptionInfo();
var installationId = new Guid("78900000-0000-0000-0000-000000000123");
var mockLicensingService = CreateMockLicensingService();
var license = new OrganizationLicense(organization, subscriptionInfo, installationId, mockLicensingService);
license.Expires = DateTime.MaxValue; // Prevent expiration during test
var globalSettings = Substitute.For<IGlobalSettings>();
globalSettings.Installation.Returns(new GlobalSettings.InstallationSettings
{
Id = installationId
});
// Act & Assert - Verify VerifyData passes with the UseDisableSmAdsForUsers value
Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings));
}
}

View File

@@ -88,7 +88,7 @@ public class UpdateOrganizationLicenseCommandTests
"Hash", "Signature", "SignatureBytes", "InstallationId", "Expires",
"ExpirationWithoutGracePeriod", "Token", "LimitCollectionCreationDeletion",
"LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems",
"UseOrganizationDomains", "UseAdminSponsoredFamilies", "UseAutomaticUserConfirmation", "UsePhishingBlocker") &&
"UseOrganizationDomains", "UseAdminSponsoredFamilies", "UseAutomaticUserConfirmation", "UsePhishingBlocker", "UseDisableSmAdsForUsers") &&
// Same property but different name, use explicit mapping
org.ExpirationDate == license.Expires));
}

View File

@@ -407,4 +407,85 @@ public class UpdateBillingAddressCommandTests
options => options.Type == TaxIdType.SpanishNIF &&
options.Value == input.TaxId.Value));
}
[Fact]
public async Task Run_BusinessOrganization_UpdatingWithSameTaxId_DeletesBeforeCreating()
{
var organization = new Organization
{
PlanType = PlanType.EnterpriseAnnually,
GatewayCustomerId = "cus_123",
GatewaySubscriptionId = "sub_123"
};
var input = new BillingAddress
{
Country = "US",
PostalCode = "12345",
Line1 = "123 Main St.",
Line2 = "Suite 100",
City = "New York",
State = "NY",
TaxId = new TaxID("us_ein", "987654321")
};
var existingTaxId = new TaxId { Id = "tax_id_123", Type = "us_ein", Value = "987654321" };
var customer = new Customer
{
Address = new Address
{
Country = "US",
PostalCode = "12345",
Line1 = "123 Main St.",
Line2 = "Suite 100",
City = "New York",
State = "NY"
},
Id = organization.GatewayCustomerId,
Subscriptions = new StripeList<Subscription>
{
Data =
[
new Subscription
{
Id = organization.GatewaySubscriptionId,
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }
}
]
},
TaxIds = new StripeList<TaxId>
{
Data = [existingTaxId]
}
};
_stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Matches(input) &&
options.HasExpansions("subscriptions", "tax_ids") &&
options.TaxExempt == TaxExempt.None
)).Returns(customer);
var newTaxId = new TaxId { Id = "tax_id_456", Type = "us_ein", Value = "987654321" };
_stripeAdapter.CreateTaxIdAsync(customer.Id, Arg.Is<TaxIdCreateOptions>(
options => options.Type == "us_ein" && options.Value == "987654321"
)).Returns(newTaxId);
var result = await _command.Run(organization, input);
Assert.True(result.IsT0);
var output = result.AsT0;
Assert.Equivalent(input, output);
// Verify that deletion happens before creation
Received.InOrder(() =>
{
_stripeAdapter.DeleteTaxIdAsync(customer.Id, existingTaxId.Id);
_stripeAdapter.CreateTaxIdAsync(customer.Id, Arg.Any<TaxIdCreateOptions>());
});
await _stripeAdapter.Received(1).DeleteTaxIdAsync(customer.Id, existingTaxId.Id);
await _stripeAdapter.Received(1).CreateTaxIdAsync(customer.Id, Arg.Is<TaxIdCreateOptions>(
options => options.Type == "us_ein" && options.Value == "987654321"));
}
}

View File

@@ -0,0 +1,339 @@
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Stripe;
using Xunit;
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;
namespace Bit.Core.Test.Billing.Premium.Commands;
public class UpdatePremiumStorageCommandTests
{
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
private readonly IUserService _userService = Substitute.For<IUserService>();
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
private readonly PremiumPlan _premiumPlan;
private readonly UpdatePremiumStorageCommand _command;
public UpdatePremiumStorageCommandTests()
{
// Setup default premium plan with standard pricing
_premiumPlan = new PremiumPlan
{
Name = "Premium",
Available = true,
LegacyYear = null,
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = "price_premium", Provided = 1 },
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = "price_storage", Provided = 1 }
};
_pricingClient.ListPremiumPlans().Returns(new List<PremiumPlan> { _premiumPlan });
_command = new UpdatePremiumStorageCommand(
_stripeAdapter,
_userService,
_pricingClient,
Substitute.For<ILogger<UpdatePremiumStorageCommand>>());
}
private Subscription CreateMockSubscription(string subscriptionId, int? storageQuantity = null)
{
var items = new List<SubscriptionItem>();
// Always add the seat item
items.Add(new SubscriptionItem
{
Id = "si_seat",
Price = new Price { Id = "price_premium" },
Quantity = 1
});
// Add storage item if quantity is provided
if (storageQuantity.HasValue && storageQuantity.Value > 0)
{
items.Add(new SubscriptionItem
{
Id = "si_storage",
Price = new Price { Id = "price_storage" },
Quantity = storageQuantity.Value
});
}
return new Subscription
{
Id = subscriptionId,
Items = new StripeList<SubscriptionItem>
{
Data = items
}
};
}
[Theory, BitAutoData]
public async Task Run_UserNotPremium_ReturnsBadRequest(User user)
{
// Arrange
user.Premium = false;
// Act
var result = await _command.Run(user, 5);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Equal("User does not have a premium subscription.", badRequest.Response);
}
[Theory, BitAutoData]
public async Task Run_NegativeStorage_ReturnsBadRequest(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 5;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
// Act
var result = await _command.Run(user, -5);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Equal("Additional storage cannot be negative.", badRequest.Response);
}
[Theory, BitAutoData]
public async Task Run_StorageExceedsMaximum_ReturnsBadRequest(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 5;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
// Act
var result = await _command.Run(user, 100);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Equal("Maximum storage is 100 GB.", badRequest.Response);
}
[Theory, BitAutoData]
public async Task Run_NoMaxStorageGb_ReturnsBadRequest(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = null;
// Act
var result = await _command.Run(user, 5);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Equal("No access to storage.", badRequest.Response);
}
[Theory, BitAutoData]
public async Task Run_StorageExceedsCurrentUsage_ReturnsBadRequest(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 10;
user.Storage = 5L * 1024 * 1024 * 1024; // 5 GB currently used
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 9);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
// Act
var result = await _command.Run(user, 0);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Contains("You are currently using", badRequest.Response);
Assert.Contains("Delete some stored data first", badRequest.Response);
}
[Theory, BitAutoData]
public async Task Run_SameStorageAmount_Idempotent(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 5;
user.Storage = 2L * 1024 * 1024 * 1024;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
// Act
var result = await _command.Run(user, 4);
// Assert
Assert.True(result.IsT0);
// Verify subscription was fetched but NOT updated
await _stripeAdapter.Received(1).GetSubscriptionAsync("sub_123");
await _stripeAdapter.DidNotReceive().UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());
}
[Theory, BitAutoData]
public async Task Run_IncreaseStorage_Success(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 5;
user.Storage = 2L * 1024 * 1024 * 1024;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
// Act
var result = await _command.Run(user, 9);
// Assert
Assert.True(result.IsT0);
// Verify subscription was updated
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_123",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Count == 1 &&
opts.Items[0].Id == "si_storage" &&
opts.Items[0].Quantity == 9 &&
opts.ProrationBehavior == "create_prorations"));
// Verify user was saved
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>
u.Id == user.Id &&
u.MaxStorageGb == 10));
}
[Theory, BitAutoData]
public async Task Run_AddStorageFromZero_Success(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 1;
user.Storage = 500L * 1024 * 1024;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", null);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
// Act
var result = await _command.Run(user, 9);
// Assert
Assert.True(result.IsT0);
// Verify subscription was updated with new storage item
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_123",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Count == 1 &&
opts.Items[0].Price == "price_storage" &&
opts.Items[0].Quantity == 9));
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 10));
}
[Theory, BitAutoData]
public async Task Run_DecreaseStorage_Success(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 10;
user.Storage = 2L * 1024 * 1024 * 1024;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 9);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
// Act
var result = await _command.Run(user, 2);
// Assert
Assert.True(result.IsT0);
// Verify subscription was updated
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_123",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Count == 1 &&
opts.Items[0].Id == "si_storage" &&
opts.Items[0].Quantity == 2));
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 3));
}
[Theory, BitAutoData]
public async Task Run_RemoveAllAdditionalStorage_Success(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 10;
user.Storage = 500L * 1024 * 1024;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 9);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
// Act
var result = await _command.Run(user, 0);
// Assert
Assert.True(result.IsT0);
// Verify subscription item was deleted
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_123",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items.Count == 1 &&
opts.Items[0].Id == "si_storage" &&
opts.Items[0].Deleted == true));
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 1));
}
[Theory, BitAutoData]
public async Task Run_MaximumStorage_Success(User user)
{
// Arrange
user.Premium = true;
user.MaxStorageGb = 5;
user.Storage = 2L * 1024 * 1024 * 1024;
user.GatewaySubscriptionId = "sub_123";
var subscription = CreateMockSubscription("sub_123", 4);
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
// Act
var result = await _command.Run(user, 99);
// Assert
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
"sub_123",
Arg.Is<SubscriptionUpdateOptions>(opts =>
opts.Items[0].Quantity == 99));
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 100));
}
}

View File

@@ -1,11 +1,14 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Subscriptions.Commands;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Test.Billing.Mocks;
using NSubstitute;
using Stripe;
using Xunit;
@@ -17,20 +20,19 @@ using static StripeConstants;
public class RestartSubscriptionCommandTests
{
private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();
private readonly IProviderRepository _providerRepository = Substitute.For<IProviderRepository>();
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>();
private readonly RestartSubscriptionCommand _command;
public RestartSubscriptionCommandTests()
{
_command = new RestartSubscriptionCommand(
Substitute.For<Microsoft.Extensions.Logging.ILogger<RestartSubscriptionCommand>>(),
_organizationRepository,
_providerRepository,
_pricingClient,
_stripeAdapter,
_subscriberService,
_userRepository);
_subscriberService);
}
[Fact]
@@ -63,11 +65,56 @@ public class RestartSubscriptionCommandTests
}
[Fact]
public async Task Run_Organization_Success_ReturnsNone()
public async Task Run_Provider_ReturnsUnhandledWithNotSupportedException()
{
var provider = new Provider { Id = Guid.NewGuid() };
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123"
};
_subscriberService.GetSubscription(provider).Returns(existingSubscription);
var result = await _command.Run(provider);
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.IsType<NotSupportedException>(unhandled.Exception);
}
[Fact]
public async Task Run_User_ReturnsUnhandledWithNotSupportedException()
{
var user = new User { Id = Guid.NewGuid() };
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123"
};
_subscriberService.GetSubscription(user).Returns(existingSubscription);
var result = await _command.Run(user);
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.IsType<NotSupportedException>(unhandled.Exception);
}
[Fact]
public async Task Run_Organization_MissingPasswordManagerItem_ReturnsUnhandledWithConflictException()
{
var organizationId = Guid.NewGuid();
var organization = new Organization { Id = organizationId };
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually
};
var plan = MockPlans.Get(PlanType.EnterpriseAnnually);
var existingSubscription = new Subscription
{
@@ -77,11 +124,122 @@ public class RestartSubscriptionCommandTests
{
Data =
[
new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 },
new SubscriptionItem { Price = new Price { Id = "price_2" }, Quantity = 2 }
new SubscriptionItem { Price = new Price { Id = "some-other-price-id" }, Quantity = 10 }
]
},
Metadata = new Dictionary<string, string> { ["key"] = "value" }
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
var result = await _command.Run(organization);
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.IsType<ConflictException>(unhandled.Exception);
Assert.Equal("Organization's subscription does not have a Password Manager subscription item.", unhandled.Exception.Message);
}
[Fact]
public async Task Run_Organization_PlanNotFound_ReturnsUnhandledWithConflictException()
{
var organizationId = Guid.NewGuid();
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually
};
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = "some-price-id" }, Quantity = 10 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
// Return a plan list that doesn't contain the organization's plan type
_pricingClient.ListPlans().Returns([MockPlans.Get(PlanType.TeamsAnnually)]);
var result = await _command.Run(organization);
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.IsType<ConflictException>(unhandled.Exception);
Assert.Equal("Could not find plan for organization's plan type", unhandled.Exception.Message);
}
[Fact]
public async Task Run_Organization_DisabledPlanWithNoEnabledReplacement_ReturnsUnhandledWithConflictException()
{
var organizationId = Guid.NewGuid();
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually2023
};
var oldPlan = new DisabledEnterprisePlan2023(true);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_old",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeSeatPlanId }, Quantity = 20 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
// Return only the disabled plan, with no enabled replacement
_pricingClient.ListPlans().Returns([oldPlan]);
var result = await _command.Run(organization);
Assert.True(result.IsT3);
var unhandled = result.AsT3;
Assert.IsType<ConflictException>(unhandled.Exception);
Assert.Equal("Could not find the current, enabled plan for organization's tier and cadence", unhandled.Exception.Message);
}
[Fact]
public async Task Run_Organization_WithNonDisabledPlan_PasswordManagerOnly_Success()
{
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually
};
var plan = MockPlans.Get(PlanType.EnterpriseAnnually);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 10 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
@@ -89,30 +247,26 @@ public class RestartSubscriptionCommandTests
Id = "sub_new",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is((SubscriptionCreateOptions options) =>
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.CollectionMethod == CollectionMethod.ChargeAutomatically &&
options.Customer == "cus_123" &&
options.Items.Count == 2 &&
options.Items[0].Price == "price_1" &&
options.Items[0].Quantity == 1 &&
options.Items[1].Price == "price_2" &&
options.Items[1].Quantity == 2 &&
options.Metadata["key"] == "value" &&
options.Items.Count == 1 &&
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 10 &&
options.Metadata["organizationId"] == organizationId.ToString() &&
options.OffSession == true &&
options.TrialPeriodDays == 0));
@@ -120,96 +274,417 @@ public class RestartSubscriptionCommandTests
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_new" &&
org.Enabled == true &&
org.ExpirationDate == currentPeriodEnd));
org.ExpirationDate == currentPeriodEnd &&
org.PlanType == PlanType.EnterpriseAnnually));
}
[Fact]
public async Task Run_Provider_Success_ReturnsNone()
public async Task Run_Organization_WithNonDisabledPlan_WithStorage_Success()
{
var providerId = Guid.NewGuid();
var provider = new Provider { Id = providerId };
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }]
},
Metadata = new Dictionary<string, string>()
};
var newSubscription = new Subscription
{
Id = "sub_new",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }
]
}
};
_subscriberService.GetSubscription(provider).Returns(existingSubscription);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(provider);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _providerRepository.Received(1).ReplaceAsync(Arg.Is<Provider>(prov =>
prov.Id == providerId &&
prov.GatewaySubscriptionId == "sub_new" &&
prov.Enabled == true));
}
[Fact]
public async Task Run_User_Success_ReturnsNone()
{
var userId = Guid.NewGuid();
var user = new User { Id = userId };
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.TeamsAnnually
};
var plan = MockPlans.Get(PlanType.TeamsAnnually);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_123",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }]
},
Metadata = new Dictionary<string, string>()
};
var newSubscription = new Subscription
{
Id = "sub_new",
CustomerId = "cus_456",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 5 },
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 3 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
{
Id = "sub_new_2",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(user).Returns(existingSubscription);
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(user);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Items.Count == 2 &&
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 5 &&
options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&
options.Items[1].Quantity == 3));
await _userRepository.Received(1).ReplaceAsync(Arg.Is<User>(u =>
u.Id == userId &&
u.GatewaySubscriptionId == "sub_new" &&
u.Premium == true &&
u.PremiumExpirationDate == currentPeriodEnd));
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_new_2" &&
org.Enabled == true));
}
[Fact]
public async Task Run_Organization_WithSecretsManager_Success()
{
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseMonthly
};
var plan = MockPlans.Get(PlanType.EnterpriseMonthly);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_789",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 15 },
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 2 },
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 10 },
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeServiceAccountPlanId }, Quantity = 100 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
{
Id = "sub_new_3",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Items.Count == 4 &&
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 15 &&
options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&
options.Items[1].Quantity == 2 &&
options.Items[2].Price == plan.SecretsManager.StripeSeatPlanId &&
options.Items[2].Quantity == 10 &&
options.Items[3].Price == plan.SecretsManager.StripeServiceAccountPlanId &&
options.Items[3].Quantity == 100));
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_new_3" &&
org.Enabled == true));
}
[Fact]
public async Task Run_Organization_WithDisabledPlan_UpgradesToNewPlan_Success()
{
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually2023
};
var oldPlan = new DisabledEnterprisePlan2023(true);
var newPlan = MockPlans.Get(PlanType.EnterpriseAnnually);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_old",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeSeatPlanId }, Quantity = 20 },
new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeStoragePlanId }, Quantity = 5 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
{
Id = "sub_upgraded",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([oldPlan, newPlan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Items.Count == 2 &&
options.Items[0].Price == newPlan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 20 &&
options.Items[1].Price == newPlan.PasswordManager.StripeStoragePlanId &&
options.Items[1].Quantity == 5));
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_upgraded" &&
org.Enabled == true &&
org.PlanType == PlanType.EnterpriseAnnually &&
org.Plan == newPlan.Name &&
org.SelfHost == newPlan.HasSelfHost &&
org.UsePolicies == newPlan.HasPolicies &&
org.UseGroups == newPlan.HasGroups &&
org.UseDirectory == newPlan.HasDirectory &&
org.UseEvents == newPlan.HasEvents &&
org.UseTotp == newPlan.HasTotp &&
org.Use2fa == newPlan.Has2fa &&
org.UseApi == newPlan.HasApi &&
org.UseSso == newPlan.HasSso &&
org.UseOrganizationDomains == newPlan.HasOrganizationDomains &&
org.UseKeyConnector == newPlan.HasKeyConnector &&
org.UseScim == newPlan.HasScim &&
org.UseResetPassword == newPlan.HasResetPassword &&
org.UsersGetPremium == newPlan.UsersGetPremium &&
org.UseCustomPermissions == newPlan.HasCustomPermissions));
}
[Fact]
public async Task Run_Organization_WithStorageAndSecretManagerButNoServiceAccounts_Success()
{
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.TeamsAnnually
};
var plan = MockPlans.Get(PlanType.TeamsAnnually);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_complex",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 12 },
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 8 },
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 6 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
{
Id = "sub_complex",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Items.Count == 3 &&
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 12 &&
options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&
options.Items[1].Quantity == 8 &&
options.Items[2].Price == plan.SecretsManager.StripeSeatPlanId &&
options.Items[2].Quantity == 6));
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_complex" &&
org.Enabled == true));
}
[Fact]
public async Task Run_Organization_WithSecretsManagerOnly_NoServiceAccounts_Success()
{
var organizationId = Guid.NewGuid();
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.TeamsMonthly
};
var plan = MockPlans.Get(PlanType.TeamsMonthly);
var existingSubscription = new Subscription
{
Status = SubscriptionStatus.Canceled,
CustomerId = "cus_sm",
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 8 },
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 5 }
]
},
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
};
var newSubscription = new Subscription
{
Id = "sub_sm",
Items = new StripeList<SubscriptionItem>
{
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_pricingClient.ListPlans().Returns([plan]);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Items.Count == 2 &&
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.Items[0].Quantity == 8 &&
options.Items[1].Price == plan.SecretsManager.StripeSeatPlanId &&
options.Items[1].Quantity == 5));
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
org.Id == organizationId &&
org.GatewaySubscriptionId == "sub_sm" &&
org.Enabled == true));
}
private record DisabledEnterprisePlan2023 : Bit.Core.Models.StaticStore.Plan
{
public DisabledEnterprisePlan2023(bool isAnnual)
{
Type = PlanType.EnterpriseAnnually2023;
ProductTier = ProductTierType.Enterprise;
Name = "Enterprise (Annually) 2023";
IsAnnual = isAnnual;
NameLocalizationKey = "planNameEnterprise";
DescriptionLocalizationKey = "planDescEnterprise";
CanBeUsedByBusiness = true;
TrialPeriodDays = 7;
HasPolicies = true;
HasSelfHost = true;
HasGroups = true;
HasDirectory = true;
HasEvents = true;
HasTotp = true;
Has2fa = true;
HasApi = true;
HasSso = true;
HasOrganizationDomains = true;
HasKeyConnector = true;
HasScim = true;
HasResetPassword = true;
UsersGetPremium = true;
HasCustomPermissions = true;
UpgradeSortOrder = 4;
DisplaySortOrder = 4;
LegacyYear = 2024;
Disabled = true;
PasswordManager = new PasswordManagerFeatures(isAnnual);
SecretsManager = new SecretsManagerFeatures(isAnnual);
}
private record SecretsManagerFeatures : SecretsManagerPlanFeatures
{
public SecretsManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 200;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually-2023";
StripeServiceAccountPlanId = "secrets-manager-service-account-2023-annually";
SeatPrice = 144;
AdditionalPricePerServiceAccount = 12;
}
else
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly-2023";
StripeServiceAccountPlanId = "secrets-manager-service-account-2023-monthly";
SeatPrice = 13;
AdditionalPricePerServiceAccount = 1;
}
}
}
private record PasswordManagerFeatures : PasswordManagerPlanFeatures
{
public PasswordManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BaseStorageGb = 1;
HasAdditionalStorageOption = true;
HasAdditionalSeatsOption = true;
AllowSeatAutoscale = true;
if (isAnnual)
{
AdditionalStoragePricePerGb = 4;
StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2023-enterprise-org-seat-annually-old";
SeatPrice = 72;
}
else
{
StripeSeatPlanId = "2023-enterprise-seat-monthly-old";
StripeStoragePlanId = "storage-gb-monthly";
SeatPrice = 7;
AdditionalStoragePricePerGb = 0.5M;
}
}
}
}
}

View File

@@ -1,12 +1,15 @@
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.Services.NoopImplementations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Dirt.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Dirt.Services.NoopImplementations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Utilities;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
@@ -19,7 +22,7 @@ using StackExchange.Redis;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations;
namespace Bit.Core.Test.Dirt.EventIntegrations;
public class EventIntegrationServiceCollectionExtensionsTests
{
@@ -200,7 +203,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
Assert.True(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
@@ -214,7 +218,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
["GlobalSettings:EventLogging:RabbitMq:HostName"] = null,
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
@@ -228,7 +233,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = null,
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
@@ -242,21 +248,38 @@ public class EventIntegrationServiceCollectionExtensionsTests
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = null,
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingExchangeName_ReturnsFalse()
public void IsRabbitMqEnabled_MissingEventExchangeName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = null
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = null,
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingIntegrationExchangeName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = null
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
@@ -268,7 +291,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
Assert.True(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
@@ -280,19 +304,34 @@ public class EventIntegrationServiceCollectionExtensionsTests
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = null,
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
}
[Fact]
public void IsAzureServiceBusEnabled_MissingTopicName_ReturnsFalse()
public void IsAzureServiceBusEnabled_MissingEventTopicName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = null
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = null,
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
}
[Fact]
public void IsAzureServiceBusEnabled_MissingIntegrationTopicName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = null
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
@@ -601,7 +640,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
// Add prerequisites
@@ -624,7 +664,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
// Add prerequisites
@@ -650,8 +691,10 @@ public class EventIntegrationServiceCollectionExtensionsTests
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration",
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
// Add prerequisites
@@ -694,7 +737,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
services.AddEventWriteServices(globalSettings);
@@ -712,7 +756,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
services.AddEventWriteServices(globalSettings);
@@ -769,10 +814,12 @@ public class EventIntegrationServiceCollectionExtensionsTests
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration",
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
services.AddEventWriteServices(globalSettings);
@@ -789,7 +836,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
});
// Add prerequisites
@@ -826,7 +874,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
});
// Add prerequisites

View File

@@ -1,9 +1,10 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Dirt.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -11,7 +12,7 @@ using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class CreateOrganizationIntegrationConfigurationCommandTests

View File

@@ -1,8 +1,9 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -10,7 +11,7 @@ using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class DeleteOrganizationIntegrationConfigurationCommandTests

View File

@@ -1,13 +1,13 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
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.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class GetOrganizationIntegrationConfigurationsQueryTests

View File

@@ -1,9 +1,10 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Dirt.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -11,7 +12,7 @@ using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class UpdateOrganizationIntegrationConfigurationCommandTests

View File

@@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Enums;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -10,7 +10,7 @@ using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
[SutProviderCustomize]
public class CreateOrganizationIntegrationCommandTests

View File

@@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Enums;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -10,7 +10,7 @@ using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
[SutProviderCustomize]
public class DeleteOrganizationIntegrationCommandTests

View File

@@ -1,12 +1,12 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Repositories;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Dirt.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
[SutProviderCustomize]
public class GetOrganizationIntegrationsQueryTests

View File

@@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Enums;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -10,7 +10,7 @@ using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
[SutProviderCustomize]
public class UpdateOrganizationIntegrationCommandTests

View File

@@ -0,0 +1,128 @@
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
public class IntegrationHandlerResultTests
{
[Theory, BitAutoData]
public void Succeed_SetsSuccessTrue_CategoryNull(IntegrationMessage message)
{
var result = IntegrationHandlerResult.Succeed(message);
Assert.True(result.Success);
Assert.Null(result.Category);
Assert.Equal(message, result.Message);
Assert.Null(result.FailureReason);
}
[Theory, BitAutoData]
public void Fail_WithCategory_SetsSuccessFalse_CategorySet(IntegrationMessage message)
{
var category = IntegrationFailureCategory.AuthenticationFailed;
var failureReason = "Invalid credentials";
var result = IntegrationHandlerResult.Fail(message, category, failureReason);
Assert.False(result.Success);
Assert.Equal(category, result.Category);
Assert.Equal(failureReason, result.FailureReason);
Assert.Equal(message, result.Message);
}
[Theory, BitAutoData]
public void Fail_WithDelayUntil_SetsDelayUntilDate(IntegrationMessage message)
{
var delayUntil = DateTime.UtcNow.AddMinutes(5);
var result = IntegrationHandlerResult.Fail(
message,
IntegrationFailureCategory.RateLimited,
"Rate limited",
delayUntil
);
Assert.Equal(delayUntil, result.DelayUntilDate);
}
[Theory, BitAutoData]
public void Retryable_RateLimited_ReturnsTrue(IntegrationMessage message)
{
var result = IntegrationHandlerResult.Fail(
message,
IntegrationFailureCategory.RateLimited,
"Rate limited"
);
Assert.True(result.Retryable);
}
[Theory, BitAutoData]
public void Retryable_TransientError_ReturnsTrue(IntegrationMessage message)
{
var result = IntegrationHandlerResult.Fail(
message,
IntegrationFailureCategory.TransientError,
"Temporary network issue"
);
Assert.True(result.Retryable);
}
[Theory, BitAutoData]
public void Retryable_AuthenticationFailed_ReturnsFalse(IntegrationMessage message)
{
var result = IntegrationHandlerResult.Fail(
message,
IntegrationFailureCategory.AuthenticationFailed,
"Invalid token"
);
Assert.False(result.Retryable);
}
[Theory, BitAutoData]
public void Retryable_ConfigurationError_ReturnsFalse(IntegrationMessage message)
{
var result = IntegrationHandlerResult.Fail(
message,
IntegrationFailureCategory.ConfigurationError,
"Channel not found"
);
Assert.False(result.Retryable);
}
[Theory, BitAutoData]
public void Retryable_ServiceUnavailable_ReturnsTrue(IntegrationMessage message)
{
var result = IntegrationHandlerResult.Fail(
message,
IntegrationFailureCategory.ServiceUnavailable,
"Service is down"
);
Assert.True(result.Retryable);
}
[Theory, BitAutoData]
public void Retryable_PermanentFailure_ReturnsFalse(IntegrationMessage message)
{
var result = IntegrationHandlerResult.Fail(
message,
IntegrationFailureCategory.PermanentFailure,
"Permanent failure"
);
Assert.False(result.Retryable);
}
[Theory, BitAutoData]
public void Retryable_SuccessCase_ReturnsFalse(IntegrationMessage message)
{
var result = IntegrationHandlerResult.Succeed(message);
Assert.False(result.Retryable);
}
}

View File

@@ -1,9 +1,9 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Xunit;
namespace Bit.Core.Test.Models.Data.EventIntegrations;
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
public class IntegrationMessageTests
{

View File

@@ -1,12 +1,12 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Time.Testing;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations;
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
public class IntegrationOAuthStateTests
{

View File

@@ -1,13 +1,13 @@
#nullable enable
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations;
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
public class IntegrationTemplateContextTests
{

View File

@@ -1,8 +1,8 @@
using System.Text.Json;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Xunit;
namespace Bit.Core.Test.Models.Data.Organizations;
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
public class OrganizationIntegrationConfigurationDetailsTests
{

View File

@@ -1,6 +1,7 @@
using Bit.Core.Enums;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
public class TestListenerConfiguration : IIntegrationListenerConfiguration
{

View File

@@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Models.Teams;
using Bit.Core.Dirt.Models.Data.Teams;
using Microsoft.Bot.Connector.Authentication;
using Xunit;
namespace Bit.Core.Test.Models.Data.Teams;
namespace Bit.Core.Test.Dirt.Models.Data.Teams;
public class TeamsBotCredentialProviderTests
{

View File

@@ -2,9 +2,10 @@
using System.Text.Json;
using Azure.Messaging.ServiceBus;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
@@ -12,7 +13,7 @@ using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class AzureServiceBusEventListenerServiceTests

View File

@@ -2,8 +2,10 @@
using System.Text.Json;
using Azure.Messaging.ServiceBus;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Services;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
@@ -11,7 +13,7 @@ using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class AzureServiceBusIntegrationListenerServiceTests
@@ -78,8 +80,10 @@ public class AzureServiceBusIntegrationListenerServiceTests
var sutProvider = GetSutProvider();
message.RetryCount = 0;
var result = new IntegrationHandlerResult(false, message);
result.Retryable = false;
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.AuthenticationFailed, // NOT retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
@@ -89,6 +93,12 @@ public class AzureServiceBusIntegrationListenerServiceTests
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());
_logger.Received().Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - non-recoverable error or max retries exceeded.")),
Arg.Any<Exception?>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Theory, BitAutoData]
@@ -96,9 +106,10 @@ public class AzureServiceBusIntegrationListenerServiceTests
{
var sutProvider = GetSutProvider();
message.RetryCount = _config.MaxRetries;
var result = new IntegrationHandlerResult(false, message);
result.Retryable = true;
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.TransientError, // Retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
@@ -108,6 +119,12 @@ public class AzureServiceBusIntegrationListenerServiceTests
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());
_logger.Received().Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - non-recoverable error or max retries exceeded.")),
Arg.Any<Exception?>(),
Arg.Any<Func<object, Exception?, string>>());
}
[Theory, BitAutoData]
@@ -116,8 +133,10 @@ public class AzureServiceBusIntegrationListenerServiceTests
var sutProvider = GetSutProvider();
message.RetryCount = 0;
var result = new IntegrationHandlerResult(false, message);
result.Retryable = true;
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.TransientError, // Retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
@@ -133,7 +152,7 @@ public class AzureServiceBusIntegrationListenerServiceTests
public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)
{
var sutProvider = GetSutProvider();
var result = new IntegrationHandlerResult(true, message);
var result = IntegrationHandlerResult.Succeed(message);
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
@@ -156,7 +175,7 @@ public class AzureServiceBusIntegrationListenerServiceTests
_logger.Received(1).Log(
LogLevel.Error,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Unhandled error processing ASB message")),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>());

View File

@@ -1,8 +1,8 @@
#nullable enable
using System.Net;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Services;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
@@ -11,7 +11,7 @@ using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class DatadogIntegrationHandlerTests
@@ -51,7 +51,7 @@ public class DatadogIntegrationHandlerTests
Assert.True(result.Success);
Assert.Equal(result.Message, message);
Assert.Empty(result.FailureReason);
Assert.Null(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(DatadogIntegrationHandler.HttpClientName))

View File

@@ -1,12 +1,13 @@
using System.Text.Json;
using Bit.Core.Dirt.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class EventIntegrationEventWriteServiceTests

View File

@@ -2,14 +2,15 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Dirt.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -19,7 +20,7 @@ using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class EventIntegrationHandlerTests

View File

@@ -1,4 +1,5 @@
using Bit.Core.Models.Data;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -6,7 +7,7 @@ using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class EventRepositoryHandlerTests

View File

@@ -1,9 +1,9 @@
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Models.Data;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
public class IntegrationFilterFactoryTests
{

View File

@@ -1,13 +1,13 @@
#nullable enable
using System.Text.Json;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
public class IntegrationFilterServiceTests
{

View File

@@ -0,0 +1,145 @@
using System.Net;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Services;
using Xunit;
namespace Bit.Core.Test.Dirt.Services;
public class IntegrationHandlerTests
{
[Fact]
public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage()
{
var sut = new TestIntegrationHandler();
var expected = new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
{
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
MessageId = "TestMessageId",
OrganizationId = "TestOrganizationId",
IntegrationType = IntegrationType.Webhook,
RenderedTemplate = "Template",
DelayUntilDate = null,
RetryCount = 0
};
var result = await sut.HandleAsync(expected.ToJson());
var typedResult = Assert.IsType<IntegrationMessage<WebhookIntegrationConfigurationDetails>>(result.Message);
Assert.Equal(expected.MessageId, typedResult.MessageId);
Assert.Equal(expected.OrganizationId, typedResult.OrganizationId);
Assert.Equal(expected.Configuration, typedResult.Configuration);
Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate);
Assert.Equal(expected.IntegrationType, typedResult.IntegrationType);
}
[Theory]
[InlineData(HttpStatusCode.Unauthorized)]
[InlineData(HttpStatusCode.Forbidden)]
public void ClassifyHttpStatusCode_AuthenticationFailed(HttpStatusCode code)
{
Assert.Equal(
IntegrationFailureCategory.AuthenticationFailed,
TestIntegrationHandler.Classify(code));
}
[Theory]
[InlineData(HttpStatusCode.NotFound)]
[InlineData(HttpStatusCode.Gone)]
[InlineData(HttpStatusCode.MovedPermanently)]
[InlineData(HttpStatusCode.TemporaryRedirect)]
[InlineData(HttpStatusCode.PermanentRedirect)]
public void ClassifyHttpStatusCode_ConfigurationError(HttpStatusCode code)
{
Assert.Equal(
IntegrationFailureCategory.ConfigurationError,
TestIntegrationHandler.Classify(code));
}
[Fact]
public void ClassifyHttpStatusCode_TooManyRequests_IsRateLimited()
{
Assert.Equal(
IntegrationFailureCategory.RateLimited,
TestIntegrationHandler.Classify(HttpStatusCode.TooManyRequests));
}
[Fact]
public void ClassifyHttpStatusCode_RequestTimeout_IsTransient()
{
Assert.Equal(
IntegrationFailureCategory.TransientError,
TestIntegrationHandler.Classify(HttpStatusCode.RequestTimeout));
}
[Theory]
[InlineData(HttpStatusCode.InternalServerError)]
[InlineData(HttpStatusCode.BadGateway)]
[InlineData(HttpStatusCode.GatewayTimeout)]
public void ClassifyHttpStatusCode_Common5xx_AreTransient(HttpStatusCode code)
{
Assert.Equal(
IntegrationFailureCategory.TransientError,
TestIntegrationHandler.Classify(code));
}
[Fact]
public void ClassifyHttpStatusCode_ServiceUnavailable_IsServiceUnavailable()
{
Assert.Equal(
IntegrationFailureCategory.ServiceUnavailable,
TestIntegrationHandler.Classify(HttpStatusCode.ServiceUnavailable));
}
[Fact]
public void ClassifyHttpStatusCode_NotImplemented_IsPermanentFailure()
{
Assert.Equal(
IntegrationFailureCategory.PermanentFailure,
TestIntegrationHandler.Classify(HttpStatusCode.NotImplemented));
}
[Fact]
public void FClassifyHttpStatusCode_Unhandled3xx_IsConfigurationError()
{
Assert.Equal(
IntegrationFailureCategory.ConfigurationError,
TestIntegrationHandler.Classify(HttpStatusCode.Found));
}
[Fact]
public void ClassifyHttpStatusCode_Unhandled4xx_IsConfigurationError()
{
Assert.Equal(
IntegrationFailureCategory.ConfigurationError,
TestIntegrationHandler.Classify(HttpStatusCode.BadRequest));
}
[Fact]
public void ClassifyHttpStatusCode_Unhandled5xx_IsServiceUnavailable()
{
Assert.Equal(
IntegrationFailureCategory.ServiceUnavailable,
TestIntegrationHandler.Classify(HttpStatusCode.HttpVersionNotSupported));
}
[Fact]
public void ClassifyHttpStatusCode_UnknownCode_DefaultsToServiceUnavailable()
{
// cast an out-of-range value to ensure default path is stable
Assert.Equal(
IntegrationFailureCategory.ServiceUnavailable,
TestIntegrationHandler.Classify((HttpStatusCode)799));
}
private class TestIntegrationHandler : IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
{
public override Task<IntegrationHandlerResult> HandleAsync(
IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
return Task.FromResult(IntegrationHandlerResult.Succeed(message: message));
}
public static IntegrationFailureCategory Classify(HttpStatusCode code) => ClassifyHttpStatusCode(code);
}
}

View File

@@ -1,11 +1,11 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Enums;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Services.Implementations;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Services;
namespace Bit.Core.Test.Dirt.Services;
public class OrganizationIntegrationConfigurationValidatorTests
{

View File

@@ -1,9 +1,10 @@
#nullable enable
using System.Text.Json;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
@@ -13,7 +14,7 @@ using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class RabbitMqEventListenerServiceTests

View File

@@ -1,8 +1,10 @@
#nullable enable
using System.Text;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Services;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
@@ -13,7 +15,7 @@ using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class RabbitMqIntegrationListenerServiceTests
@@ -86,8 +88,10 @@ public class RabbitMqIntegrationListenerServiceTests
new BasicProperties(),
body: Encoding.UTF8.GetBytes(message.ToJson())
);
var result = new IntegrationHandlerResult(false, message);
result.Retryable = false;
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.AuthenticationFailed, // NOT retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
@@ -105,7 +109,7 @@ public class RabbitMqIntegrationListenerServiceTests
_logger.Received().Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Non-retryable failure")),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - non-retryable.")),
Arg.Any<Exception?>(),
Arg.Any<Func<object, Exception?, string>>());
@@ -133,8 +137,10 @@ public class RabbitMqIntegrationListenerServiceTests
new BasicProperties(),
body: Encoding.UTF8.GetBytes(message.ToJson())
);
var result = new IntegrationHandlerResult(false, message);
result.Retryable = true;
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.TransientError, // Retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
@@ -151,7 +157,7 @@ public class RabbitMqIntegrationListenerServiceTests
_logger.Received().Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Max retry attempts reached")),
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - max retries exceeded.")),
Arg.Any<Exception?>(),
Arg.Any<Func<object, Exception?, string>>());
@@ -179,9 +185,10 @@ public class RabbitMqIntegrationListenerServiceTests
new BasicProperties(),
body: Encoding.UTF8.GetBytes(message.ToJson())
);
var result = new IntegrationHandlerResult(false, message);
result.Retryable = true;
result.DelayUntilDate = _now.AddMinutes(1);
var result = IntegrationHandlerResult.Fail(
message: message,
category: IntegrationFailureCategory.TransientError, // Retryable
failureReason: "403");
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
@@ -220,7 +227,7 @@ public class RabbitMqIntegrationListenerServiceTests
new BasicProperties(),
body: Encoding.UTF8.GetBytes(message.ToJson())
);
var result = new IntegrationHandlerResult(true, message);
var result = IntegrationHandlerResult.Succeed(message);
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);

View File

@@ -1,13 +1,14 @@
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Models.Slack;
using Bit.Core.Services;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Models.Data.Slack;
using Bit.Core.Dirt.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class SlackIntegrationHandlerTests
@@ -110,7 +111,7 @@ public class SlackIntegrationHandlerTests
}
[Fact]
public async Task HandleAsync_NullResponse_ReturnsNonRetryableFailure()
public async Task HandleAsync_NullResponse_ReturnsRetryableFailure()
{
var sutProvider = GetSutProvider();
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
@@ -126,7 +127,7 @@ public class SlackIntegrationHandlerTests
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.False(result.Retryable);
Assert.True(result.Retryable); // Null response is classified as TransientError (retryable)
Assert.Equal("Slack response was null", result.FailureReason);
Assert.Equal(result.Message, message);

View File

@@ -3,7 +3,7 @@
using System.Net;
using System.Text.Json;
using System.Web;
using Bit.Core.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.MockedHttpClient;
@@ -11,7 +11,7 @@ using NSubstitute;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class SlackServiceTests

View File

@@ -1,5 +1,7 @@
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Services;
using System.Text.Json;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
@@ -8,7 +10,7 @@ using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class TeamsIntegrationHandlerTests
@@ -42,9 +44,77 @@ public class TeamsIntegrationHandlerTests
);
}
[Theory, BitAutoData]
public async Task HandleAsync_ArgumentException_ReturnsConfigurationError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
sutProvider.GetDependency<ITeamsService>()
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
.ThrowsAsync(new ArgumentException("argument error"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.ConfigurationError, result.Category);
Assert.False(result.Retryable);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
);
}
[Theory, BitAutoData]
public async Task HandleAsync_HttpExceptionNonRetryable_ReturnsFalseAndNotRetryable(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
public async Task HandleAsync_JsonException_ReturnsPermanentFailure(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
sutProvider.GetDependency<ITeamsService>()
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
.ThrowsAsync(new JsonException("JSON error"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.PermanentFailure, result.Category);
Assert.False(result.Retryable);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
);
}
[Theory, BitAutoData]
public async Task HandleAsync_UriFormatException_ReturnsConfigurationError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
sutProvider.GetDependency<ITeamsService>()
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
.ThrowsAsync(new UriFormatException("Bad URI"));
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.ConfigurationError, result.Category);
Assert.False(result.Retryable);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
);
}
[Theory, BitAutoData]
public async Task HandleAsync_HttpExceptionForbidden_ReturnsAuthenticationFailed(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
@@ -62,6 +132,7 @@ public class TeamsIntegrationHandlerTests
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.AuthenticationFailed, result.Category);
Assert.False(result.Retryable);
Assert.Equal(result.Message, message);
@@ -73,7 +144,7 @@ public class TeamsIntegrationHandlerTests
}
[Theory, BitAutoData]
public async Task HandleAsync_HttpExceptionRetryable_ReturnsFalseAndRetryable(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
public async Task HandleAsync_HttpExceptionTooManyRequests_ReturnsRateLimited(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
@@ -92,6 +163,7 @@ public class TeamsIntegrationHandlerTests
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.Equal(IntegrationFailureCategory.RateLimited, result.Category);
Assert.True(result.Retryable);
Assert.Equal(result.Message, message);
@@ -103,7 +175,7 @@ public class TeamsIntegrationHandlerTests
}
[Theory, BitAutoData]
public async Task HandleAsync_UnknownException_ReturnsFalseAndNotRetryable(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
public async Task HandleAsync_UnknownException_ReturnsTransientError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
@@ -114,7 +186,8 @@ public class TeamsIntegrationHandlerTests
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.False(result.Retryable);
Assert.Equal(IntegrationFailureCategory.TransientError, result.Category);
Assert.True(result.Retryable);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(

View File

@@ -3,11 +3,11 @@
using System.Net;
using System.Text.Json;
using System.Web;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Models.Teams;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Dirt.Entities;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Models.Data.Teams;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.MockedHttpClient;
@@ -15,7 +15,7 @@ using NSubstitute;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class TeamsServiceTests

View File

@@ -1,7 +1,7 @@
using System.Net;
using System.Net.Http.Headers;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Services;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
@@ -10,7 +10,7 @@ using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
namespace Bit.Core.Test.Dirt.Services;
[SutProviderCustomize]
public class WebhookIntegrationHandlerTests
@@ -51,7 +51,7 @@ public class WebhookIntegrationHandlerTests
Assert.True(result.Success);
Assert.Equal(result.Message, message);
Assert.Empty(result.FailureReason);
Assert.Null(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
@@ -79,7 +79,7 @@ public class WebhookIntegrationHandlerTests
Assert.True(result.Success);
Assert.Equal(result.Message, message);
Assert.Empty(result.FailureReason);
Assert.Null(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))

View File

@@ -0,0 +1,151 @@
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Authorization;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.KeyManagement.Authorization;
[SutProviderCustomize]
public class KeyConnectorAuthorizationHandlerTests
{
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserCanUseKeyConnector_Success(
User user,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
sutProvider.GetDependency<ICurrentContext>().Organizations
.Returns(new List<CurrentContextOrganization>());
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserAlreadyUsesKeyConnector_Fails(
User user,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = true;
sutProvider.GetDependency<ICurrentContext>().Organizations
.Returns(new List<CurrentContextOrganization>());
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserIsOwner_Fails(
User user,
Guid organizationId,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
var organizations = new List<CurrentContextOrganization>
{
new() { Id = organizationId, Type = OrganizationUserType.Owner }
};
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserIsAdmin_Fails(
User user,
Guid organizationId,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
var organizations = new List<CurrentContextOrganization>
{
new() { Id = organizationId, Type = OrganizationUserType.Admin }
};
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserIsRegularMember_Success(
User user,
Guid organizationId,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
var organizations = new List<CurrentContextOrganization>
{
new() { Id = organizationId, Type = OrganizationUserType.User }
};
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UnsupportedRequirement_ThrowsArgumentException(
User user,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
sutProvider.GetDependency<ICurrentContext>().Organizations
.Returns(new List<CurrentContextOrganization>());
var unsupportedRequirement = new KeyConnectorOperationsRequirement("UnsupportedOperation");
var context = new AuthorizationHandlerContext([unsupportedRequirement], claimsPrincipal, user);
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(context));
}
}

View File

@@ -0,0 +1,125 @@
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Commands;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.KeyManagement.Commands;
[SutProviderCustomize]
public class SetKeyConnectorKeyCommandTests
{
[Theory, BitAutoData]
public async Task SetKeyConnectorKeyForUserAsync_Success_SetsAccountKeys(
User user,
KeyConnectorKeysData data,
SutProvider<SetKeyConnectorKeyCommand> sutProvider)
{
// Set up valid V2 encryption data
if (data.AccountKeys!.SignatureKeyPair != null)
{
data.AccountKeys.SignatureKeyPair.SignatureAlgorithm = "ed25519";
}
var expectedAccountKeysData = data.AccountKeys.ToAccountKeysData();
// Arrange
user.UsesKeyConnector = false;
var currentContext = sutProvider.GetDependency<ICurrentContext>();
var httpContext = Substitute.For<HttpContext>();
httpContext.User.Returns(new ClaimsPrincipal());
currentContext.HttpContext.Returns(httpContext);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())
.Returns(AuthorizationResult.Success());
var userRepository = sutProvider.GetDependency<IUserRepository>();
var mockUpdateUserData = Substitute.For<UpdateUserData>();
userRepository.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey!)
.Returns(mockUpdateUserData);
// Act
await sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data);
// Assert
userRepository
.Received(1)
.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey);
await userRepository
.Received(1)
.SetV2AccountCryptographicStateAsync(
user.Id,
Arg.Is<UserAccountKeysData>(data =>
data.PublicKeyEncryptionKeyPairData.PublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.PublicKey &&
data.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey &&
data.PublicKeyEncryptionKeyPairData.SignedPublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey &&
data.SignatureKeyPairData!.SignatureAlgorithm == expectedAccountKeysData.SignatureKeyPairData!.SignatureAlgorithm &&
data.SignatureKeyPairData.WrappedSigningKey == expectedAccountKeysData.SignatureKeyPairData.WrappedSigningKey &&
data.SignatureKeyPairData.VerifyingKey == expectedAccountKeysData.SignatureKeyPairData.VerifyingKey &&
data.SecurityStateData!.SecurityState == expectedAccountKeysData.SecurityStateData!.SecurityState &&
data.SecurityStateData.SecurityVersion == expectedAccountKeysData.SecurityStateData.SecurityVersion),
Arg.Is<IEnumerable<UpdateUserData>>(actions =>
actions.Count() == 1 && actions.First() == mockUpdateUserData));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
await sutProvider.GetDependency<IAcceptOrgUserCommand>()
.Received(1)
.AcceptOrgUserByOrgSsoIdAsync(data.OrgIdentifier, user, sutProvider.GetDependency<IUserService>());
}
[Theory, BitAutoData]
public async Task SetKeyConnectorKeyForUserAsync_UserCantUseKeyConnector_ThrowsException(
User user,
KeyConnectorKeysData data,
SutProvider<SetKeyConnectorKeyCommand> sutProvider)
{
// Arrange
user.UsesKeyConnector = true;
var currentContext = sutProvider.GetDependency<ICurrentContext>();
var httpContext = Substitute.For<HttpContext>();
httpContext.User.Returns(new ClaimsPrincipal());
currentContext.HttpContext.Returns(httpContext);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())
.Returns(AuthorizationResult.Failed());
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data));
sutProvider.GetDependency<IUserRepository>()
.DidNotReceiveWithAnyArgs()
.SetKeyConnectorUserKey(Arg.Any<Guid>(), Arg.Any<string>());
await sutProvider.GetDependency<IUserRepository>()
.DidNotReceiveWithAnyArgs()
.SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>(), Arg.Any<IEnumerable<UpdateUserData>>());
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogUserEventAsync(Arg.Any<Guid>(), Arg.Any<EventType>());
await sutProvider.GetDependency<IAcceptOrgUserCommand>()
.DidNotReceiveWithAnyArgs()
.AcceptOrgUserByOrgSsoIdAsync(Arg.Any<string>(), Arg.Any<User>(), Arg.Any<IUserService>());
}
}

View File

@@ -2,6 +2,7 @@
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
@@ -242,4 +243,134 @@ public class UpgradeOrganizationPlanCommandTests
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(default);
}
[Theory]
[FreeOrganizationUpgradeCustomize, BitAutoData]
public async Task UpgradePlan_WhenOrganizationIsMissingPublicAndPrivateKeys_Backfills(
Organization organization,
OrganizationUpgrade upgrade,
string newPublicKey,
string newPrivateKey,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
organization.PublicKey = null;
organization.PrivateKey = null;
upgrade.Plan = PlanType.TeamsAnnually;
upgrade.Keys = new PublicKeyEncryptionKeyPairData(
wrappedPrivateKey: newPrivateKey,
publicKey: newPublicKey);
upgrade.AdditionalSeats = 10;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(organization.PlanType)
.Returns(MockPlans.Get(organization.PlanType));
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(upgrade.Plan)
.Returns(MockPlans.Get(upgrade.Plan));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 });
// Act
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
// Assert
Assert.Equal(newPublicKey, organization.PublicKey);
Assert.Equal(newPrivateKey, organization.PrivateKey);
await sutProvider.GetDependency<IOrganizationService>()
.Received(1)
.ReplaceAndUpdateCacheAsync(organization);
}
[Theory]
[FreeOrganizationUpgradeCustomize, BitAutoData]
public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotOverwriteWithNull(
Organization organization,
OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
// Arrange
const string existingPublicKey = "existing-public-key";
const string existingPrivateKey = "existing-private-key";
organization.PublicKey = existingPublicKey;
organization.PrivateKey = existingPrivateKey;
upgrade.Plan = PlanType.TeamsAnnually;
upgrade.Keys = null;
upgrade.AdditionalSeats = 10;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(organization.PlanType)
.Returns(MockPlans.Get(organization.PlanType));
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(upgrade.Plan)
.Returns(MockPlans.Get(upgrade.Plan));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 });
// Act
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
// Assert
Assert.Equal(existingPublicKey, organization.PublicKey);
Assert.Equal(existingPrivateKey, organization.PrivateKey);
await sutProvider.GetDependency<IOrganizationService>()
.Received(1)
.ReplaceAndUpdateCacheAsync(organization);
}
[Theory]
[FreeOrganizationUpgradeCustomize, BitAutoData]
public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotBackfillWithNewKeys(
Organization organization,
OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
// Arrange
const string existingPublicKey = "existing-public-key";
const string existingPrivateKey = "existing-private-key";
const string newPublicKey = "new-public-key";
const string newPrivateKey = "new-private-key";
organization.PublicKey = existingPublicKey;
organization.PrivateKey = existingPrivateKey;
upgrade.Plan = PlanType.TeamsAnnually;
upgrade.Keys = new PublicKeyEncryptionKeyPairData(
wrappedPrivateKey: newPrivateKey,
publicKey: newPublicKey);
upgrade.AdditionalSeats = 10;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(organization.PlanType)
.Returns(MockPlans.Get(organization.PlanType));
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(upgrade.Plan)
.Returns(MockPlans.Get(upgrade.Plan));
sutProvider.GetDependency<IOrganizationRepository>()
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 });
// Act
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
// Assert
Assert.Equal(existingPublicKey, organization.PublicKey);
Assert.Equal(existingPrivateKey, organization.PrivateKey);
await sutProvider.GetDependency<IOrganizationService>()
.Received(1)
.ReplaceAndUpdateCacheAsync(organization);
}
}

View File

@@ -9,5 +9,5 @@ public class TestMailView : BaseMailView
public class TestMail : BaseMail<TestMailView>
{
public override string Subject { get; } = "Test Email";
public override string Subject { get; set; } = "Test Email";
}

View File

@@ -25,11 +25,15 @@ using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Fido2NetLib;
using Fido2NetLib.Objects;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using NSubstitute;
using Xunit;
using static Fido2NetLib.Fido2;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.Services;
@@ -594,6 +598,209 @@ public class UserServiceTests
user.MasterPassword = null;
}
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task StartWebAuthnRegistrationAsync_BelowLimit_Succeeds(
bool hasPremium, SutProvider<UserService> sutProvider, User user)
{
// Arrange - Non-premium user with 4 credentials (below limit of 5)
SetupWebAuthnProvider(user, credentialCount: 4);
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = new GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
user.Premium = hasPremium;
user.Id = Guid.NewGuid();
user.Email = "test@example.com";
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(user.Id)
.Returns(new List<OrganizationUser>());
var mockFido2 = sutProvider.GetDependency<IFido2>();
mockFido2.RequestNewCredential(
Arg.Any<Fido2User>(),
Arg.Any<List<PublicKeyCredentialDescriptor>>(),
Arg.Any<AuthenticatorSelection>(),
Arg.Any<AttestationConveyancePreference>())
.Returns(new CredentialCreateOptions
{
Challenge = new byte[] { 1, 2, 3 },
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
User = new Fido2User
{
Id = user.Id.ToByteArray(),
Name = user.Email,
DisplayName = user.Name
},
PubKeyCredParams = new List<PubKeyCredParam>()
});
// Act
var result = await sutProvider.Sut.StartWebAuthnRegistrationAsync(user);
// Assert
Assert.NotNull(result);
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(user);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task CompleteWebAuthRegistrationAsync_ExceedsLimit_ThrowsBadRequestException(bool hasPremium,
SutProvider<UserService> sutProvider, User user, AuthenticatorAttestationRawResponse deviceResponse)
{
// Arrange - time-of-check/time-of-use scenario: user now has 10 credentials (at limit)
SetupWebAuthnProviderWithPending(user, credentialCount: 10);
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = new GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
user.Premium = hasPremium;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(user.Id)
.Returns(new List<OrganizationUser>());
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CompleteWebAuthRegistrationAsync(user, 11, "NewKey", deviceResponse));
Assert.Equal("Maximum allowed WebAuthn credential count exceeded.", exception.Message);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task CompleteWebAuthRegistrationAsync_BelowLimit_Succeeds(bool hasPremium,
SutProvider<UserService> sutProvider, User user, AuthenticatorAttestationRawResponse deviceResponse)
{
// Arrange - User has 4 credentials (below limit of 5)
SetupWebAuthnProviderWithPending(user, credentialCount: 4);
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = new GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
user.Premium = hasPremium;
user.Id = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(user.Id)
.Returns(new List<OrganizationUser>());
var mockFido2 = sutProvider.GetDependency<IFido2>();
mockFido2.MakeNewCredentialAsync(
Arg.Any<AuthenticatorAttestationRawResponse>(),
Arg.Any<CredentialCreateOptions>(),
Arg.Any<IsCredentialIdUniqueToUserAsyncDelegate>())
.Returns(new CredentialMakeResult("ok", "", new AttestationVerificationSuccess
{
Aaguid = Guid.NewGuid(),
Counter = 0,
CredentialId = new byte[] { 1, 2, 3 },
CredType = "public-key",
PublicKey = new byte[] { 4, 5, 6 },
Status = "ok",
User = new Fido2User
{
Id = user.Id.ToByteArray(),
Name = user.Email ?? "test@example.com",
DisplayName = user.Name ?? "Test User"
}
}));
// Act
var result = await sutProvider.Sut.CompleteWebAuthRegistrationAsync(user, 5, "NewKey", deviceResponse);
// Assert
Assert.True(result);
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(user);
}
private static void SetupWebAuthnProvider(User user, int credentialCount)
{
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
var metadata = new Dictionary<string, object>();
// Add credentials as Key1, Key2, Key3, etc.
for (int i = 1; i <= credentialCount; i++)
{
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
{
Name = $"Key {i}",
Descriptor = new PublicKeyCredentialDescriptor(new byte[] { (byte)i }),
PublicKey = new byte[] { (byte)i },
UserHandle = new byte[] { (byte)i },
SignatureCounter = 0,
CredType = "public-key",
RegDate = DateTime.UtcNow,
AaGuid = Guid.NewGuid()
};
}
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider
{
Enabled = true,
MetaData = metadata
};
user.SetTwoFactorProviders(providers);
}
private static void SetupWebAuthnProviderWithPending(User user, int credentialCount)
{
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
var metadata = new Dictionary<string, object>();
// Add existing credentials
for (int i = 1; i <= credentialCount; i++)
{
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
{
Name = $"Key {i}",
Descriptor = new PublicKeyCredentialDescriptor(new byte[] { (byte)i }),
PublicKey = new byte[] { (byte)i },
UserHandle = new byte[] { (byte)i },
SignatureCounter = 0,
CredType = "public-key",
RegDate = DateTime.UtcNow,
AaGuid = Guid.NewGuid()
};
}
// Add pending registration
var pendingOptions = new CredentialCreateOptions
{
Challenge = new byte[] { 1, 2, 3 },
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
User = new Fido2User
{
Id = user.Id.ToByteArray(),
Name = user.Email ?? "test@example.com",
DisplayName = user.Name ?? "Test User"
},
PubKeyCredParams = new List<PubKeyCredParam>()
};
metadata["pending"] = pendingOptions.ToJson();
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider
{
Enabled = true,
MetaData = metadata
};
user.SetTwoFactorProviders(providers);
}
}
public static class UserServiceSutProviderExtensions

View File

@@ -0,0 +1,169 @@
using System.Security.Claims;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.SendFeatures.Queries;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Tools.Services;
public class SendOwnerQueryTests
{
private readonly ISendRepository _sendRepository;
private readonly IFeatureService _featureService;
private readonly IUserService _userService;
private readonly SendOwnerQuery _sendOwnerQuery;
private readonly Guid _currentUserId = Guid.NewGuid();
private readonly ClaimsPrincipal _user;
public SendOwnerQueryTests()
{
_sendRepository = Substitute.For<ISendRepository>();
_featureService = Substitute.For<IFeatureService>();
_userService = Substitute.For<IUserService>();
_user = new ClaimsPrincipal();
_userService.GetProperUserId(_user).Returns(_currentUserId);
_sendOwnerQuery = new SendOwnerQuery(_sendRepository, _featureService, _userService);
}
[Fact]
public async Task Get_WithValidSendOwnedByUser_ReturnsExpectedSend()
{
// Arrange
var sendId = Guid.NewGuid();
var expectedSend = CreateSend(sendId, _currentUserId);
_sendRepository.GetByIdAsync(sendId).Returns(expectedSend);
// Act
var result = await _sendOwnerQuery.Get(sendId, _user);
// Assert
Assert.Same(expectedSend, result);
await _sendRepository.Received(1).GetByIdAsync(sendId);
}
[Fact]
public async Task Get_WithNonExistentSend_ThrowsNotFoundException()
{
// Arrange
var sendId = Guid.NewGuid();
_sendRepository.GetByIdAsync(sendId).Returns((Send?)null);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() => _sendOwnerQuery.Get(sendId, _user));
}
[Fact]
public async Task Get_WithSendOwnedByDifferentUser_ThrowsNotFoundException()
{
// Arrange
var sendId = Guid.NewGuid();
var differentUserId = Guid.NewGuid();
var send = CreateSend(sendId, differentUserId);
_sendRepository.GetByIdAsync(sendId).Returns(send);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() => _sendOwnerQuery.Get(sendId, _user));
}
[Fact]
public async Task Get_WithNullCurrentUserId_ThrowsBadRequestException()
{
// Arrange
var sendId = Guid.NewGuid();
var send = CreateSend(sendId, _currentUserId);
_sendRepository.GetByIdAsync(sendId).Returns(send);
var nullUser = new ClaimsPrincipal();
_userService.GetProperUserId(nullUser).Returns((Guid?)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sendOwnerQuery.Get(sendId, nullUser));
Assert.Equal("invalid user.", exception.Message);
}
[Fact]
public async Task GetOwned_WithFeatureFlagEnabled_ReturnsAllSends()
{
// Arrange
var sends = new List<Send>
{
CreateSend(Guid.NewGuid(), _currentUserId, emails: null),
CreateSend(Guid.NewGuid(), _currentUserId, emails: "test@example.com"),
CreateSend(Guid.NewGuid(), _currentUserId, emails: "other@example.com")
};
_sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(sends);
_featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(true);
// Act
var result = await _sendOwnerQuery.GetOwned(_user);
// Assert
Assert.Equal(3, result.Count);
Assert.Contains(sends[0], result);
Assert.Contains(sends[1], result);
Assert.Contains(sends[2], result);
await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId);
_featureService.Received(1).IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends);
}
[Fact]
public async Task GetOwned_WithFeatureFlagDisabled_FiltersOutEmailOtpSends()
{
// Arrange
var sendWithoutEmails = CreateSend(Guid.NewGuid(), _currentUserId, emails: null);
var sendWithEmails = CreateSend(Guid.NewGuid(), _currentUserId, emails: "test@example.com");
var sends = new List<Send> { sendWithoutEmails, sendWithEmails };
_sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(sends);
_featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(false);
// Act
var result = await _sendOwnerQuery.GetOwned(_user);
// Assert
Assert.Single(result);
Assert.Contains(sendWithoutEmails, result);
Assert.DoesNotContain(sendWithEmails, result);
await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId);
_featureService.Received(1).IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends);
}
[Fact]
public async Task GetOwned_WithNullCurrentUserId_ThrowsBadRequestException()
{
// Arrange
var nullUser = new ClaimsPrincipal();
_userService.GetProperUserId(nullUser).Returns((Guid?)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sendOwnerQuery.GetOwned(nullUser));
Assert.Equal("invalid user.", exception.Message);
}
[Fact]
public async Task GetOwned_WithEmptyCollection_ReturnsEmptyCollection()
{
// Arrange
var emptySends = new List<Send>();
_sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(emptySends);
_featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(true);
// Act
var result = await _sendOwnerQuery.GetOwned(_user);
// Assert
Assert.Empty(result);
await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId);
}
private static Send CreateSend(Guid id, Guid userId, string? emails = null)
{
return new Send
{
Id = id,
UserId = userId,
Emails = emails
};
}
}

View File

@@ -1,4 +1,5 @@
using Bit.Core.Enums;
using Bit.Core.Dirt.Enums;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;

View File

@@ -12,7 +12,6 @@ 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())
@@ -28,7 +27,6 @@ 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())

View File

@@ -16,16 +16,15 @@ namespace Bit.Core.Test.Vault.Commands;
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,
[BitAutoData(true, 1, 1, 1)]
[BitAutoData(false, 1, 0, 1)]
[BitAutoData(false, 1, 0, 1)]
[BitAutoData(true, 1, 0, 1)]
public async Task ArchiveManyAsync_Works(
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 };
@@ -46,4 +45,33 @@ public class ArchiveCiphersCommandTest
await sutProvider.GetDependency<IPushNotificationService>().Received(pushNotificationsCalls)
.PushSyncCiphersAsync(user.Id);
}
[Theory]
[BitAutoData]
public async Task ArchiveManyAsync_SetsArchivedDateOnReturnedCiphers(
SutProvider<ArchiveCiphersCommand> sutProvider,
CipherDetails cipher,
User user)
{
// Allow organization cipher to be archived in this test
cipher.OrganizationId = Guid.Parse("3f2504e0-4f89-11d3-9a0c-0305e82c3301");
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(user.Id)
.Returns(new List<CipherDetails> { cipher });
var repoRevisionDate = DateTime.UtcNow;
sutProvider.GetDependency<ICipherRepository>()
.ArchiveAsync(Arg.Any<IEnumerable<Guid>>(), user.Id)
.Returns(repoRevisionDate);
// Act
var result = await sutProvider.Sut.ArchiveManyAsync(new[] { cipher.Id }, user.Id);
// Assert
var archivedCipher = Assert.Single(result);
Assert.Equal(repoRevisionDate, archivedCipher.RevisionDate);
Assert.Equal(repoRevisionDate, archivedCipher.ArchivedDate);
}
}

View File

@@ -16,16 +16,15 @@ namespace Bit.Core.Test.Vault.Commands;
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)]
[BitAutoData(true, 1, 1, 1)]
[BitAutoData(false, 1, 0, 1)]
[BitAutoData(false, 1, 0, 1)]
[BitAutoData(true, 1, 1, 1)]
public async Task UnarchiveAsync_Works(
bool isEditable, bool hasOrganizationId,
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 };
@@ -46,4 +45,33 @@ public class UnarchiveCiphersCommandTest
await sutProvider.GetDependency<IPushNotificationService>().Received(pushNotificationsCalls)
.PushSyncCiphersAsync(user.Id);
}
[Theory]
[BitAutoData]
public async Task UnarchiveAsync_ClearsArchivedDateOnReturnedCiphers(
SutProvider<UnarchiveCiphersCommand> sutProvider,
CipherDetails cipher,
User user)
{
cipher.OrganizationId = null;
cipher.ArchivedDate = DateTime.UtcNow;
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(user.Id)
.Returns(new List<CipherDetails> { cipher });
var repoRevisionDate = DateTime.UtcNow.AddMinutes(1);
sutProvider.GetDependency<ICipherRepository>()
.UnarchiveAsync(Arg.Any<IEnumerable<Guid>>(), user.Id)
.Returns(repoRevisionDate);
// Act
var result = await sutProvider.Sut.UnarchiveManyAsync(new[] { cipher.Id }, user.Id);
// Assert
var unarchivedCipher = Assert.Single(result);
Assert.Equal(repoRevisionDate, unarchivedCipher.RevisionDate);
Assert.Null(unarchivedCipher.ArchivedDate);
}
}

View File

@@ -1190,6 +1190,7 @@ public class CipherServiceTests
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
UsePolicies = true,
PlanType = PlanType.EnterpriseAnnually,
MaxStorageGb = 100
});
@@ -1206,6 +1207,140 @@ public class CipherServiceTests
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}
[Theory, BitAutoData]
public async Task ShareManyAsync_StorageLimitBypass_Passes(SutProvider<CipherService> sutProvider,
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually,
UsePolicies = true,
MaxStorageGb = 3,
Storage = 3221225472 // 3 GB used, so 0 remaining
});
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;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(sharingUserId)
.Returns(new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Enabled,
[new PolicyDetails
{
OrganizationId = organizationId,
PolicyType = PolicyType.OrganizationDataOwnership,
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
}]));
await sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId);
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}
[Theory, BitAutoData]
public async Task ShareManyAsync_StorageLimit_Enforced(SutProvider<CipherService> sutProvider,
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually,
UsePolicies = true,
MaxStorageGb = 3,
Storage = 3221225472 // 3 GB used, so 0 remaining
});
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;
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(sharingUserId)
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, []));
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId)
);
Assert.Contains("Not enough storage available for this organization.", exception.Message);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpdateCiphersAsync(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}
[Theory, BitAutoData]
public async Task ShareManyAsync_StorageLimit_Enforced_WhenFeatureFlagDisabled(SutProvider<CipherService> sutProvider,
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually,
UsePolicies = true,
MaxStorageGb = 3,
Storage = 3221225472 // 3 GB used, so 0 remaining
});
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;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId)
);
Assert.Contains("Not enough storage available for this organization.", exception.Message);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpdateCiphersAsync(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}
[Theory, BitAutoData]
public async Task ShareManyAsync_StorageLimit_Enforced_WhenUsePoliciesDisabled(SutProvider<CipherService> sutProvider,
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually,
UsePolicies = false,
MaxStorageGb = 3,
Storage = 3221225472 // 3 GB used, so 0 remaining
});
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;
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(true);
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId)
);
Assert.Contains("Not enough storage available for this organization.", exception.Message);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpdateCiphersAsync(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}
private class SaveDetailsAsyncDependencies
{
public CipherDetails CipherDetails { get; set; }
@@ -1215,12 +1350,12 @@ public class CipherServiceTests
private static SaveDetailsAsyncDependencies GetSaveDetailsAsyncDependencies(
SutProvider<CipherService> sutProvider,
string newPassword,
bool viewPassword,
bool editPermission,
bool permission,
string? key = null,
string? totp = null,
CipherLoginFido2CredentialData[]? passkeys = null,
CipherFieldData[]? fields = null
CipherFieldData[]? fields = null,
string? existingKey = "OriginalKey"
)
{
var cipherDetails = new CipherDetails
@@ -1233,13 +1368,22 @@ public class CipherServiceTests
Key = key,
};
var newLoginData = new CipherLoginData { Username = "user", Password = newPassword, Totp = totp, Fido2Credentials = passkeys, Fields = fields };
var newLoginData = new CipherLoginData
{
Username = "user",
Password = newPassword,
Totp = totp,
Fido2Credentials = passkeys,
Fields = fields
};
cipherDetails.Data = JsonSerializer.Serialize(newLoginData);
var existingCipher = new Cipher
{
Id = cipherDetails.Id,
Type = CipherType.Login,
Key = existingKey,
Data = JsonSerializer.Serialize(
new CipherLoginData
{
@@ -1261,7 +1405,14 @@ public class CipherServiceTests
var permissions = new Dictionary<Guid, OrganizationCipherPermission>
{
{ cipherDetails.Id, new OrganizationCipherPermission { ViewPassword = viewPassword, Edit = editPermission } }
{
cipherDetails.Id,
new OrganizationCipherPermission
{
ViewPassword = permission,
Edit = permission
}
}
};
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>()
@@ -1278,7 +1429,7 @@ public class CipherServiceTests
[Theory, BitAutoData]
public async Task SaveDetailsAsync_PasswordNotChangedWithoutViewPasswordPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: true);
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false);
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1294,7 +1445,7 @@ public class CipherServiceTests
[Theory, BitAutoData]
public async Task SaveDetailsAsync_PasswordNotChangedWithoutEditPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false);
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false);
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1310,7 +1461,7 @@ public class CipherServiceTests
[Theory, BitAutoData]
public async Task SaveDetailsAsync_PasswordChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true);
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true);
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1326,7 +1477,11 @@ public class CipherServiceTests
[Theory, BitAutoData]
public async Task SaveDetailsAsync_CipherKeyChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, "NewKey");
var deps = GetSaveDetailsAsyncDependencies(
sutProvider,
newPassword: "NewPassword",
permission: true,
key: "NewKey");
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1336,27 +1491,40 @@ public class CipherServiceTests
true);
Assert.Equal("NewKey", deps.CipherDetails.Key);
await sutProvider.GetDependency<ICipherRepository>()
.Received()
.ReplaceAsync(Arg.Is<CipherDetails>(c => c.Id == deps.CipherDetails.Id && c.Key == "NewKey"));
}
[Theory, BitAutoData]
public async Task SaveDetailsAsync_CipherKeyChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
public async Task SaveDetailsAsync_CipherKeyNotChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, "NewKey");
var deps = GetSaveDetailsAsyncDependencies(
sutProvider,
newPassword: "NewPassword",
permission: false,
key: "NewKey"
);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => deps.SutProvider.Sut.SaveDetailsAsync(
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
deps.CipherDetails.UserId.Value,
deps.CipherDetails.RevisionDate,
null,
true));
true);
Assert.Contains("do not have permission", exception.Message);
Assert.Equal("OriginalKey", deps.CipherDetails.Key);
await sutProvider.GetDependency<ICipherRepository>()
.Received()
.ReplaceAsync(Arg.Is<CipherDetails>(c => c.Id == deps.CipherDetails.Id && c.Key == "OriginalKey"));
}
[Theory, BitAutoData]
public async Task SaveDetailsAsync_TotpChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, totp: "NewTotp");
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, totp: "NewTotp");
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1372,7 +1540,7 @@ public class CipherServiceTests
[Theory, BitAutoData]
public async Task SaveDetailsAsync_TotpChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, totp: "NewTotp");
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, totp: "NewTotp");
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1397,7 +1565,7 @@ public class CipherServiceTests
}
};
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, passkeys: passkeys);
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, passkeys: passkeys);
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1422,7 +1590,7 @@ public class CipherServiceTests
}
};
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, passkeys: passkeys);
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, passkeys: passkeys);
await deps.SutProvider.Sut.SaveDetailsAsync(
deps.CipherDetails,
@@ -1439,7 +1607,7 @@ public class CipherServiceTests
[BitAutoData]
public async Task SaveDetailsAsync_HiddenFieldsChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: false, fields:
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, fields:
[
new CipherFieldData
{
@@ -1464,7 +1632,7 @@ public class CipherServiceTests
[BitAutoData]
public async Task SaveDetailsAsync_HiddenFieldsChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
{
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, fields:
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, fields:
[
new CipherFieldData
{