1
0
mirror of https://github.com/bitwarden/server synced 2026-01-04 09:33:40 +00:00

Merge branch 'main' into SM-1571-DisableSMAdsForUsers

This commit is contained in:
cd-bitwarden
2025-10-22 12:28:04 -04:00
committed by GitHub
295 changed files with 42165 additions and 4881 deletions

View File

@@ -14,10 +14,12 @@ public class PolicyRequirementQueryTests
[Theory, BitAutoData]
public async Task GetAsync_IgnoresOtherPolicyTypes(Guid userId)
{
var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };
var otherPolicy = new PolicyDetails { PolicyType = PolicyType.RequireSso };
var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId };
var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.RequireSso, UserId = userId };
var policyRepository = Substitute.For<IPolicyRepository>();
policyRepository.GetPolicyDetailsByUserId(userId).Returns([otherPolicy, thisPolicy]);
policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(userId)), PolicyType.SingleOrg)
.Returns([otherPolicy, thisPolicy]);
var factory = new TestPolicyRequirementFactory(_ => true);
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
@@ -33,9 +35,11 @@ public class PolicyRequirementQueryTests
{
// Arrange policies
var policyRepository = Substitute.For<IPolicyRepository>();
var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };
var otherPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };
policyRepository.GetPolicyDetailsByUserId(userId).Returns([thisPolicy, otherPolicy]);
var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId };
var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId };
policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(userId)), PolicyType.SingleOrg)
.Returns([thisPolicy, otherPolicy]);
// Arrange a substitute Enforce function so that we can inspect the received calls
var callback = Substitute.For<Func<PolicyDetails, bool>>();
@@ -70,7 +74,9 @@ public class PolicyRequirementQueryTests
public async Task GetAsync_HandlesNoPolicies(Guid userId)
{
var policyRepository = Substitute.For<IPolicyRepository>();
policyRepository.GetPolicyDetailsByUserId(userId).Returns([]);
policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(userId)), PolicyType.SingleOrg)
.Returns([]);
var factory = new TestPolicyRequirementFactory(x => x.IsProvider);
var sut = new PolicyRequirementQuery(policyRepository, [factory]);

View File

@@ -72,4 +72,65 @@ public class FreeFamiliesForEnterprisePolicyValidatorTests
organizationSponsorships[0].SponsoredOrganizationId.ToString(), organization.Name);
}
[Theory, BitAutoData]
public async Task ExecutePreUpsertSideEffectAsync_DoesNotNotifyUserWhenPolicyDisabled(
Organization organization,
List<OrganizationSponsorship> organizationSponsorships,
[PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate,
[Policy(PolicyType.FreeFamiliesSponsorshipPolicy, true)] Policy policy,
SutProvider<FreeFamiliesForEnterprisePolicyValidator> sutProvider)
{
policy.Enabled = true;
policyUpdate.Enabled = false;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(policyUpdate.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)
.Returns(organizationSponsorships);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
await sutProvider.GetDependency<IMailService>()
.DidNotReceiveWithAnyArgs()
.SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(default, default, default, default);
}
[Theory, BitAutoData]
public async Task ExecutePreUpsertSideEffectAsync_DoesNotifyUserWhenPolicyEnabled(
Organization organization,
List<OrganizationSponsorship> organizationSponsorships,
[PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate,
[Policy(PolicyType.FreeFamiliesSponsorshipPolicy, false)] Policy policy,
SutProvider<FreeFamiliesForEnterprisePolicyValidator> sutProvider)
{
policy.Enabled = false;
policyUpdate.Enabled = true;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(policyUpdate.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)
.Returns(organizationSponsorships);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
var offerAcceptanceDate = organizationSponsorships[0].ValidUntil!.Value.AddDays(-7).ToString("MM/dd/yyyy");
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(
organizationSponsorships[0].FriendlyName,
offerAcceptanceDate,
organizationSponsorships[0].SponsoredOrganizationId.ToString(),
organization.Name);
}
}

View File

@@ -274,4 +274,176 @@ public class OrganizationDataOwnershipPolicyValidatorTests
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, null, 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,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task ExecutePostUpsertSideEffectAsync_PolicyBeingDisabled_DoesNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task ExecutePostUpsertSideEffectAsync_WhenNoUsersExist_DoNothing(
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
OrganizationDataOwnershipPolicyRequirementFactory factory)
{
// Arrange
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
var policyRepository = ArrangePolicyRepository([]);
var collectionRepository = Substitute.For<ICollectionRepository>();
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await collectionRepository
.DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(
default,
default,
default);
await policyRepository
.Received(1)
.GetPolicyDetailsByOrganizationIdAsync(
policyUpdate.OrganizationId,
PolicyType.OrganizationDataOwnership);
}
[Theory]
[BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]
public async Task ExecutePostUpsertSideEffectAsync_WithRequirements_ShouldUpsertDefaultCollections(
Policy postUpdatedPolicy,
Policy? previousPolicyState,
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
[OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable<OrganizationPolicyDetails> orgPolicyDetails,
OrganizationDataOwnershipPolicyRequirementFactory factory)
{
// Arrange
var orgPolicyDetailsList = orgPolicyDetails.ToList();
foreach (var policyDetail in orgPolicyDetailsList)
{
policyDetail.OrganizationId = policyUpdate.OrganizationId;
}
var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);
var collectionRepository = Substitute.For<ICollectionRepository>();
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await collectionRepository
.Received(1)
.UpsertDefaultCollectionsAsync(
policyUpdate.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3),
_defaultUserCollectionName);
}
[Theory]
[BitMemberAutoData(nameof(WhenDefaultCollectionsDoesNotExistTestCases))]
public async Task ExecutePostUpsertSideEffectAsync_WhenDefaultCollectionNameIsInvalid_DoesNothing(
IPolicyMetadataModel metadata,
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
{
// Arrange
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
policyUpdate.Enabled = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
var policyRequest = new SavePolicyModel(policyUpdate, null, metadata);
// Act
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
// Assert
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertDefaultCollectionsAsync(default, default, default);
}
}

View File

@@ -72,4 +72,66 @@ public class RequireSsoPolicyValidatorTests
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.True(string.IsNullOrEmpty(result));
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorEnabled_ValidationError(
[PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.RequireSso)] Policy policy,
SutProvider<RequireSsoPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeEnabled_ValidationError(
[PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.RequireSso)] Policy policy,
SutProvider<RequireSsoPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.Contains("Trusted device encryption is on", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_DecryptionOptionsNotEnabled_Success(
[PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.RequireSso)] Policy policy,
SutProvider<RequireSsoPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = false };
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.True(string.IsNullOrEmpty(result));
}
}

View File

@@ -68,4 +68,59 @@ public class ResetPasswordPolicyValidatorTests
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);
Assert.True(string.IsNullOrEmpty(result));
}
[Theory]
[BitAutoData(true, false)]
[BitAutoData(false, true)]
[BitAutoData(false, false)]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeEnabled_ValidationError(
bool policyEnabled,
bool autoEnrollEnabled,
[PolicyUpdate(PolicyType.ResetPassword)] PolicyUpdate policyUpdate,
[Policy(PolicyType.ResetPassword)] Policy policy,
SutProvider<ResetPasswordPolicyValidator> sutProvider)
{
policyUpdate.Enabled = policyEnabled;
policyUpdate.SetDataModel(new ResetPasswordDataModel
{
AutoEnrollEnabled = autoEnrollEnabled
});
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.Contains("Trusted device encryption is on and requires this policy.", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeNotEnabled_Success(
[PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.ResetPassword)] Policy policy,
SutProvider<ResetPasswordPolicyValidator> sutProvider)
{
policyUpdate.SetDataModel(new ResetPasswordDataModel
{
AutoEnrollEnabled = false
});
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = false };
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.True(string.IsNullOrEmpty(result));
}
}

View File

@@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
@@ -145,4 +146,135 @@ public class SingleOrgPolicyValidatorTests
.Received(1)
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorEnabled_ValidationError(
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg)] Policy policy,
SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorNotEnabled_Success(
[PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg)] Policy policy,
SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = policyUpdate.OrganizationId;
var ssoConfig = new SsoConfig { Enabled = false };
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policyUpdate.OrganizationId)
.Returns(ssoConfig);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
.Returns(false);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);
Assert.True(string.IsNullOrEmpty(result));
}
[Theory, BitAutoData]
public async Task ExecutePreUpsertSideEffectAsync_RevokesNonCompliantUsers(
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg, false)] Policy policy,
Guid savingUserId,
Guid nonCompliantUserId,
Organization organization,
SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
var compliantUser1 = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = new Guid(),
Email = "user1@example.com"
};
var compliantUser2 = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = new Guid(),
Email = "user2@example.com"
};
var nonCompliantUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = nonCompliantUserId,
Email = "user3@example.com"
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([compliantUser1, compliantUser2, nonCompliantUser]);
var otherOrganizationUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = new Guid(),
UserId = nonCompliantUserId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(nonCompliantUserId)))
.Returns([otherOrganizationUser]);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
.Returns(new CommandResult());
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(
Arg.Is<RevokeOrganizationUsersRequest>(r =>
r.OrganizationId == organization.Id &&
r.OrganizationUsers.Count() == 1 &&
r.OrganizationUsers.First().Id == nonCompliantUser.Id));
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email);
}
}

View File

@@ -136,4 +136,124 @@ public class TwoFactorAuthenticationPolicyValidatorTests
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),
compliantUser.Email);
}
[Theory, BitAutoData]
public async Task ExecutePreUpsertSideEffectAsync_GivenNonCompliantUsersWithoutMasterPassword_Throws(
Organization organization,
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([orgUserDetailUserWithout2Fa]);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(orgUserDetailUserWithout2Fa, false),
});
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy));
Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, exception.Message);
}
[Theory, BitAutoData]
public async Task ExecutePreUpsertSideEffectAsync_RevokesOnlyNonCompliantUsers(
Organization organization,
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
{
// Arrange
policy.OrganizationId = policyUpdate.OrganizationId;
organization.Id = policyUpdate.OrganizationId;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var nonCompliantUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var compliantUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Email = "user4@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([nonCompliantUser, compliantUser]);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(nonCompliantUser, false),
(compliantUser, true)
});
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
.Returns(new CommandResult());
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
// Act
await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);
// Assert
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(Arg.Is<RevokeOrganizationUsersRequest>(req =>
req.OrganizationId == policyUpdate.OrganizationId &&
req.OrganizationUsers.SequenceEqual(new[] { nonCompliantUser })
));
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),
nonCompliantUser.Email);
// Did not send out an email for compliantUser
await sutProvider.GetDependency<IMailService>()
.Received(0)
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),
compliantUser.Email);
}
}

View File

@@ -28,9 +28,10 @@ public class VNextSavePolicyCommandTests
// Arrange
var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();
fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns("");
var sutProvider = SutProviderFactory(
[new FakeSingleOrgDependencyEvent()],
[fakePolicyValidationEvent]);
var sutProvider = SutProviderFactory([
new FakeSingleOrgDependencyEvent(),
fakePolicyValidationEvent
]);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
@@ -71,9 +72,10 @@ public class VNextSavePolicyCommandTests
// Arrange
var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();
fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns("");
var sutProvider = SutProviderFactory(
[new FakeSingleOrgDependencyEvent()],
[fakePolicyValidationEvent]);
var sutProvider = SutProviderFactory([
new FakeSingleOrgDependencyEvent(),
fakePolicyValidationEvent
]);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
@@ -110,23 +112,6 @@ public class VNextSavePolicyCommandTests
p.RevisionDate == revisionDate));
}
[Fact]
public void Constructor_DuplicatePolicyDependencyEvents_Throws()
{
// Arrange & Act
var exception = Assert.Throws<Exception>(() =>
new VNextSavePolicyCommand(
Substitute.For<IApplicationCacheService>(),
Substitute.For<IEventService>(),
Substitute.For<IPolicyRepository>(),
[new FakeSingleOrgDependencyEvent(), new FakeSingleOrgDependencyEvent()],
Substitute.For<TimeProvider>(),
Substitute.For<IPolicyEventHandlerFactory>()));
// Assert
Assert.Contains("Duplicate PolicyValidationEvent for SingleOrg policy", exception.Message);
}
[Theory, BitAutoData]
public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate)
{
@@ -366,9 +351,10 @@ public class VNextSavePolicyCommandTests
// Arrange
var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();
fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns("Validation error!");
var sutProvider = SutProviderFactory(
[new FakeSingleOrgDependencyEvent()],
[fakePolicyValidationEvent]);
var sutProvider = SutProviderFactory([
new FakeSingleOrgDependencyEvent(),
fakePolicyValidationEvent
]);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
@@ -392,20 +378,20 @@ public class VNextSavePolicyCommandTests
}
/// <summary>
/// Returns a new SutProvider with the PolicyDependencyEvents registered in the Sut.
/// Returns a new SutProvider with the PolicyUpdateEvents registered in the Sut.
/// </summary>
private static SutProvider<VNextSavePolicyCommand> SutProviderFactory(
IEnumerable<IEnforceDependentPoliciesEvent>? policyDependencyEvents = null,
IEnumerable<IPolicyValidationEvent>? policyValidationEvents = null)
IEnumerable<IPolicyUpdateEvent>? policyUpdateEvents = null)
{
var policyEventHandlerFactory = Substitute.For<IPolicyEventHandlerFactory>();
var handlers = policyUpdateEvents ?? [];
// Setup factory to return handlers based on type
policyEventHandlerFactory.GetHandler<IEnforceDependentPoliciesEvent>(Arg.Any<PolicyType>())
.Returns(callInfo =>
{
var policyType = callInfo.Arg<PolicyType>();
var handler = policyDependencyEvents?.FirstOrDefault(e => e.Type == policyType);
var handler = handlers.OfType<IEnforceDependentPoliciesEvent>().FirstOrDefault(e => e.Type == policyType);
return handler != null ? OneOf.OneOf<IEnforceDependentPoliciesEvent, None>.FromT0(handler) : OneOf.OneOf<IEnforceDependentPoliciesEvent, None>.FromT1(new None());
});
@@ -413,7 +399,7 @@ public class VNextSavePolicyCommandTests
.Returns(callInfo =>
{
var policyType = callInfo.Arg<PolicyType>();
var handler = policyValidationEvents?.FirstOrDefault(e => e.Type == policyType);
var handler = handlers.OfType<IPolicyValidationEvent>().FirstOrDefault(e => e.Type == policyType);
return handler != null ? OneOf.OneOf<IPolicyValidationEvent, None>.FromT0(handler) : OneOf.OneOf<IPolicyValidationEvent, None>.FromT1(new None());
});
@@ -425,7 +411,7 @@ public class VNextSavePolicyCommandTests
return new SutProvider<VNextSavePolicyCommand>()
.WithFakeTimeProvider()
.SetDependency(policyDependencyEvents ?? [])
.SetDependency(handlers)
.SetDependency(policyEventHandlerFactory)
.Create();
}

View File

@@ -467,10 +467,9 @@ public class AuthRequestServiceTests
Arg.Any<string>(),
Arg.Any<string>());
var expectedLogMessage = "There are no admin emails to send to.";
sutProvider.GetDependency<ILogger<AuthRequestService>>()
.Received(1)
.LogWarning(expectedLogMessage);
.LogWarning("There are no admin emails to send to.");
}
/// <summary>

View File

@@ -294,7 +294,8 @@ public class InvoiceExtensionsTests
Amount = 600
}
);
invoice.Tax = 120; // $1.20 in cents
invoice.TotalTaxes = [new InvoiceTotalTax { Amount = 120 }]; // $1.20 in cents
var subscription = new Subscription();
// Act
@@ -318,7 +319,7 @@ public class InvoiceExtensionsTests
Amount = 600
}
);
invoice.Tax = null;
invoice.TotalTaxes = [];
var subscription = new Subscription();
// Act
@@ -341,7 +342,7 @@ public class InvoiceExtensionsTests
Amount = 600
}
);
invoice.Tax = 0;
invoice.TotalTaxes = [new InvoiceTotalTax { Amount = 0 }];
var subscription = new Subscription();
// Act
@@ -374,7 +375,7 @@ public class InvoiceExtensionsTests
var invoice = new Invoice
{
Lines = lineItems,
Tax = 200 // Additional $2.00 tax
TotalTaxes = [new InvoiceTotalTax { Amount = 200 }] // Additional $2.00 tax
};
var subscription = new Subscription();

View File

@@ -228,8 +228,16 @@ If you believe you need to change the version for a valid reason, please discuss
Status = "active",
TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
Items = new StripeList<SubscriptionItem>
{
Data = [
new SubscriptionItem
{
CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
}
]
}
};
return new SubscriptionInfo

View File

@@ -141,8 +141,16 @@ If you believe you need to change the version for a valid reason, please discuss
Status = "active",
TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
Items = new StripeList<SubscriptionItem>
{
Data = [
new SubscriptionItem
{
CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)
}
]
}
};
return new SubscriptionInfo

View File

@@ -54,7 +54,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 500,
TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],
Total = 5500
};
@@ -77,7 +77,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2021-family-for-enterprise-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 1 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -112,7 +112,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 750,
TotalTaxes = [new InvoiceTotalTax { Amount = 750 }],
Total = 8250
};
@@ -137,7 +137,9 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "2023-teams-org-seat-monthly" && item.Quantity == 5) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-teams-seat-monthly" && item.Quantity == 3) &&
options.Coupon == CouponIDs.SecretsManagerStandalone));
options.Discounts != null &&
options.Discounts.Count == 1 &&
options.Discounts[0].Coupon == CouponIDs.SecretsManagerStandalone));
}
[Fact]
@@ -173,7 +175,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 1200,
TotalTaxes = [new InvoiceTotalTax { Amount = 1200 }],
Total = 12200
};
@@ -205,7 +207,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 8) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-service-account-2024-annually" && item.Quantity == 3) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -234,7 +236,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 300,
TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],
Total = 3300
};
@@ -257,7 +259,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2020-families-org-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 6 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -286,7 +288,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 0,
TotalTaxes = [new InvoiceTotalTax { Amount = 0 }],
Total = 2700
};
@@ -309,7 +311,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" &&
options.SubscriptionDetails.Items[0].Quantity == 3 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -339,7 +341,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 2100,
TotalTaxes = [new InvoiceTotalTax { Amount = 2100 }],
Total = 12100
};
@@ -365,7 +367,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-enterprise-seat-monthly" &&
options.SubscriptionDetails.Items[0].Quantity == 15 &&
options.Coupon == null));
options.Discounts == null));
}
#endregion
@@ -399,7 +401,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 120,
TotalTaxes = [new InvoiceTotalTax { Amount = 120 }],
Total = 1320
};
@@ -422,7 +424,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" &&
options.SubscriptionDetails.Items[0].Quantity == 2 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -452,7 +454,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 400,
TotalTaxes = [new InvoiceTotalTax { Amount = 400 }],
Total = 4400
};
@@ -475,7 +477,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2020-families-org-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 1 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -524,7 +526,11 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 900,
TotalTaxes = [new InvoiceTotalTax
{
Amount = 900
}
],
Total = 9900
};
@@ -546,7 +552,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 6 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -595,7 +601,11 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 1200,
TotalTaxes = [new InvoiceTotalTax
{
Amount = 1200
}
],
Total = 13200
};
@@ -617,7 +627,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-enterprise-org-seat-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 6 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -647,7 +657,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 800,
TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],
Total = 8800
};
@@ -672,7 +682,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 2) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 2) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -724,7 +734,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 1500,
TotalTaxes = [new InvoiceTotalTax { Amount = 1500 }],
Total = 16500
};
@@ -753,7 +763,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 5) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-service-account-2024-annually" && item.Quantity == 10) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -808,7 +818,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 600,
TotalTaxes = [new InvoiceTotalTax { Amount = 600 }],
Total = 6600
};
@@ -831,7 +841,9 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-enterprise-org-seat-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 5 &&
options.Coupon == "EXISTING_DISCOUNT_50"));
options.Discounts != null &&
options.Discounts.Count == 1 &&
options.Discounts[0].Coupon == "EXISTING_DISCOUNT_50"));
}
[Fact]
@@ -911,7 +923,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 600,
TotalTaxes = [new InvoiceTotalTax { Amount = 600 }],
Total = 6600
};
@@ -934,7 +946,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" &&
options.SubscriptionDetails.Items[0].Quantity == 10 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -976,7 +988,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 1200,
TotalTaxes = [new InvoiceTotalTax { Amount = 1200 }],
Total = 13200
};
@@ -1001,7 +1013,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 15) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "storage-gb-annually" && item.Quantity == 5) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -1043,7 +1055,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 800,
TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],
Total = 8800
};
@@ -1066,7 +1078,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "secrets-manager-teams-seat-annually" &&
options.SubscriptionDetails.Items[0].Quantity == 8 &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -1111,7 +1123,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 1500,
TotalTaxes = [new InvoiceTotalTax { Amount = 1500 }],
Total = 16500
};
@@ -1139,7 +1151,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "secrets-manager-enterprise-seat-monthly" && item.Quantity == 12) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-service-account-2024-monthly" && item.Quantity == 20) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -1192,7 +1204,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 2500,
TotalTaxes = [new InvoiceTotalTax { Amount = 2500 }],
Total = 27500
};
@@ -1224,7 +1236,9 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 15) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "secrets-manager-service-account-2024-annually" && item.Quantity == 30) &&
options.Coupon == "ENTERPRISE_DISCOUNT_20"));
options.Discounts != null &&
options.Discounts.Count == 1 &&
options.Discounts[0].Coupon == "ENTERPRISE_DISCOUNT_20"));
}
[Fact]
@@ -1266,7 +1280,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 500,
TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],
Total = 5500
};
@@ -1291,7 +1305,7 @@ public class PreviewOrganizationTaxCommandTests
item.Price == "2020-families-org-annually" && item.Quantity == 6) &&
options.SubscriptionDetails.Items.Any(item =>
item.Price == "personal-storage-gb-annually" && item.Quantity == 2) &&
options.Coupon == null));
options.Discounts == null));
}
[Fact]
@@ -1368,7 +1382,7 @@ public class PreviewOrganizationTaxCommandTests
var invoice = new Invoice
{
Tax = 300,
TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],
Total = 3300
};
@@ -1391,7 +1405,7 @@ public class PreviewOrganizationTaxCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" &&
options.SubscriptionDetails.Items[0].Quantity == 5 &&
options.Coupon == null));
options.Discounts == null));
}
#endregion

View File

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

View File

@@ -27,25 +27,27 @@ public class GetCloudOrganizationLicenseQueryTests
{
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_InvalidInstallationId_Throws(SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
public async Task GetLicenseAsync_InvalidInstallationId_Throws(
SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, int version)
{
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).ReturnsNull();
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetLicenseAsync(organization, installationId, version));
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetLicenseAsync(organization, installationId, version));
Assert.Contains("Invalid installation id", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_DisabledOrganization_Throws(SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
public async Task GetLicenseAsync_DisabledOrganization_Throws(
SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, Installation installation)
{
installation.Enabled = false;
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetLicenseAsync(organization, installationId));
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.GetLicenseAsync(organization, installationId));
Assert.Contains("Invalid installation id", exception.Message);
}
@@ -71,7 +73,8 @@ public class GetCloudOrganizationLicenseQueryTests
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_WhenFeatureFlagEnabled_CreatesToken(SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
public async Task GetLicenseAsync_WhenFeatureFlagEnabled_CreatesToken(
SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo,
byte[] licenseSignature, string token)
{
@@ -90,7 +93,8 @@ public class GetCloudOrganizationLicenseQueryTests
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_MSPManagedOrganization_UsesProviderSubscription(SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
public async Task GetLicenseAsync_MSPManagedOrganization_UsesProviderSubscription(
SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo,
byte[] licenseSignature, Provider provider)
{
@@ -99,8 +103,17 @@ public class GetCloudOrganizationLicenseQueryTests
subInfo.Subscription = new SubscriptionInfo.BillingSubscription(new Subscription
{
CurrentPeriodStart = DateTime.UtcNow,
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodStart = DateTime.UtcNow,
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)
}
]
}
});
installation.Enabled = true;

View File

@@ -272,7 +272,16 @@ public class GetOrganizationWarningsQueryTests
CollectionMethod = CollectionMethod.SendInvoice,
Customer = new Customer(),
Status = SubscriptionStatus.Active,
CurrentPeriodEnd = now.AddDays(10),
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = now.AddDays(10)
}
]
},
TestClock = new TestClock
{
FrozenTime = now

View File

@@ -1,4 +1,5 @@
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Billing.Services;
@@ -105,6 +106,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -152,6 +163,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -241,7 +262,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -286,6 +316,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -326,7 +366,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "incomplete";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -342,7 +391,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Assert
Assert.True(result.IsT0);
Assert.True(user.Premium);
Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate);
Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate);
}
[Theory, BitAutoData]
@@ -368,7 +417,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -384,7 +442,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Assert
Assert.True(result.IsT0);
Assert.True(user.Premium);
Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate);
Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate);
}
[Theory, BitAutoData]
@@ -411,7 +469,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active"; // PayPal + active doesn't match pattern
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();
@@ -453,7 +520,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "incomplete";
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
var mockInvoice = Substitute.For<Invoice>();

View File

@@ -31,7 +31,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 300,
TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],
Total = 3300
};
@@ -65,7 +65,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 500,
TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],
Total = 5500
};
@@ -101,7 +101,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 250,
TotalTaxes = [new InvoiceTotalTax { Amount = 250 }],
Total = 2750
};
@@ -135,7 +135,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 800,
TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],
Total = 8800
};
@@ -171,7 +171,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 450,
TotalTaxes = [new InvoiceTotalTax { Amount = 450 }],
Total = 4950
};
@@ -207,7 +207,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 0,
TotalTaxes = [new InvoiceTotalTax { Amount = 0 }],
Total = 3000
};
@@ -241,7 +241,7 @@ public class PreviewPremiumTaxCommandTests
var invoice = new Invoice
{
Tax = 600,
TotalTaxes = [new InvoiceTotalTax { Amount = 600 }],
Total = 6600
};
@@ -276,7 +276,7 @@ public class PreviewPremiumTaxCommandTests
// Stripe amounts are in cents
var invoice = new Invoice
{
Tax = 123, // $1.23
TotalTaxes = [new InvoiceTotalTax { Amount = 123 }], // $1.23
Total = 3123 // $31.23
};

View File

@@ -1,10 +1,15 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
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;
@@ -118,4 +123,232 @@ public class OrganizationBillingServiceTests
}
#endregion
#region Finalize - Trial Settings
[Theory, BitAutoData]
public async Task NoPaymentMethodAndTrialPeriod_SetsMissingPaymentMethodCancelBehavior(
Organization organization,
SutProvider<OrganizationBillingService> sutProvider)
{
// Arrange
var plan = StaticStore.GetPlan(PlanType.TeamsAnnually);
organization.PlanType = PlanType.TeamsAnnually;
organization.GatewayCustomerId = "cus_test123";
organization.GatewaySubscriptionId = null;
var subscriptionSetup = new SubscriptionSetup
{
PlanType = PlanType.TeamsAnnually,
PasswordManagerOptions = new SubscriptionSetup.PasswordManager
{
Seats = 5,
Storage = null,
PremiumAccess = false
},
SecretsManagerOptions = null,
SkipTrial = false
};
var sale = new OrganizationSale
{
Organization = organization,
SubscriptionSetup = subscriptionSetup
};
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(PlanType.TeamsAnnually)
.Returns(plan);
sutProvider.GetDependency<IHasPaymentMethodQuery>()
.Run(organization)
.Returns(false);
var customer = new Customer
{
Id = "cus_test123",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())
.Returns(customer);
SubscriptionCreateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionCreateAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.Returns(new Subscription
{
Id = "sub_test123",
Status = StripeConstants.SubscriptionStatus.Trialing
});
sutProvider.GetDependency<IOrganizationRepository>()
.ReplaceAsync(organization)
.Returns(Task.CompletedTask);
// Act
await sutProvider.Sut.Finalize(sale);
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
Assert.NotNull(capturedOptions);
Assert.Equal(7, capturedOptions.TrialPeriodDays);
Assert.NotNull(capturedOptions.TrialSettings);
Assert.NotNull(capturedOptions.TrialSettings.EndBehavior);
Assert.Equal("cancel", capturedOptions.TrialSettings.EndBehavior.MissingPaymentMethod);
}
[Theory, BitAutoData]
public async Task NoPaymentMethodButNoTrial_DoesNotSetMissingPaymentMethodBehavior(
Organization organization,
SutProvider<OrganizationBillingService> sutProvider)
{
// Arrange
var plan = StaticStore.GetPlan(PlanType.TeamsAnnually);
organization.PlanType = PlanType.TeamsAnnually;
organization.GatewayCustomerId = "cus_test123";
organization.GatewaySubscriptionId = null;
var subscriptionSetup = new SubscriptionSetup
{
PlanType = PlanType.TeamsAnnually,
PasswordManagerOptions = new SubscriptionSetup.PasswordManager
{
Seats = 5,
Storage = null,
PremiumAccess = false
},
SecretsManagerOptions = null,
SkipTrial = true // This will result in TrialPeriodDays = 0
};
var sale = new OrganizationSale
{
Organization = organization,
SubscriptionSetup = subscriptionSetup
};
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(PlanType.TeamsAnnually)
.Returns(plan);
sutProvider.GetDependency<IHasPaymentMethodQuery>()
.Run(organization)
.Returns(false);
var customer = new Customer
{
Id = "cus_test123",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())
.Returns(customer);
SubscriptionCreateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionCreateAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.Returns(new Subscription
{
Id = "sub_test123",
Status = StripeConstants.SubscriptionStatus.Active
});
sutProvider.GetDependency<IOrganizationRepository>()
.ReplaceAsync(organization)
.Returns(Task.CompletedTask);
// Act
await sutProvider.Sut.Finalize(sale);
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
Assert.NotNull(capturedOptions);
Assert.Equal(0, capturedOptions.TrialPeriodDays);
Assert.Null(capturedOptions.TrialSettings);
}
[Theory, BitAutoData]
public async Task HasPaymentMethodAndTrialPeriod_DoesNotSetMissingPaymentMethodBehavior(
Organization organization,
SutProvider<OrganizationBillingService> sutProvider)
{
// Arrange
var plan = StaticStore.GetPlan(PlanType.TeamsAnnually);
organization.PlanType = PlanType.TeamsAnnually;
organization.GatewayCustomerId = "cus_test123";
organization.GatewaySubscriptionId = null;
var subscriptionSetup = new SubscriptionSetup
{
PlanType = PlanType.TeamsAnnually,
PasswordManagerOptions = new SubscriptionSetup.PasswordManager
{
Seats = 5,
Storage = null,
PremiumAccess = false
},
SecretsManagerOptions = null,
SkipTrial = false
};
var sale = new OrganizationSale
{
Organization = organization,
SubscriptionSetup = subscriptionSetup
};
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(PlanType.TeamsAnnually)
.Returns(plan);
sutProvider.GetDependency<IHasPaymentMethodQuery>()
.Run(organization)
.Returns(true); // Has payment method
var customer = new Customer
{
Id = "cus_test123",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())
.Returns(customer);
SubscriptionCreateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionCreateAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.Returns(new Subscription
{
Id = "sub_test123",
Status = StripeConstants.SubscriptionStatus.Trialing
});
sutProvider.GetDependency<IOrganizationRepository>()
.ReplaceAsync(organization)
.Returns(Task.CompletedTask);
// Act
await sutProvider.Sut.Finalize(sale);
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
Assert.NotNull(capturedOptions);
Assert.Equal(7, capturedOptions.TrialPeriodDays);
Assert.Null(capturedOptions.TrialSettings);
}
#endregion
}

View File

@@ -88,7 +88,13 @@ public class RestartSubscriptionCommandTests
var newSubscription = new Subscription
{
Id = "sub_new",
CurrentPeriodEnd = currentPeriodEnd
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
}
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
@@ -138,7 +144,13 @@ public class RestartSubscriptionCommandTests
var newSubscription = new Subscription
{
Id = "sub_new",
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }
]
}
};
_subscriberService.GetSubscription(provider).Returns(existingSubscription);
@@ -177,7 +189,13 @@ public class RestartSubscriptionCommandTests
var newSubscription = new Subscription
{
Id = "sub_new",
CurrentPeriodEnd = currentPeriodEnd
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
}
};
_subscriberService.GetSubscription(user).Returns(existingSubscription);

View File

@@ -1,9 +1,11 @@
#nullable enable
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Kdf.Implementations;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
@@ -21,16 +23,12 @@ public class ChangeKdfCommandTests
[BitAutoData]
public async Task ChangeKdfAsync_ChangesKdfAsync(SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(IdentityResult.Success));
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Success));
var kdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 4,
Memory = 512,
Parallelism = 4
};
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
@@ -59,13 +57,7 @@ public class ChangeKdfCommandTests
[BitAutoData]
public async Task ChangeKdfAsync_UserIsNull_ThrowsArgumentNullException(SutProvider<ChangeKdfCommand> sutProvider)
{
var kdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 4,
Memory = 512,
Parallelism = 4
};
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
@@ -85,17 +77,13 @@ public class ChangeKdfCommandTests
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider<ChangeKdfCommand> sutProvider, User user)
public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider<ChangeKdfCommand> sutProvider,
User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(false));
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(false));
var kdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 4,
Memory = 512,
Parallelism = 4
};
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
@@ -116,7 +104,9 @@ public class ChangeKdfCommandTests
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_WithAuthenticationAndUnlockData_UpdatesUserCorrectly(SutProvider<ChangeKdfCommand> sutProvider, User user)
public async Task
ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOff_UpdatesUserCorrectlyAndLogsOut(
SutProvider<ChangeKdfCommand> sutProvider, User user)
{
var constantKdf = new KdfSettings
{
@@ -137,8 +127,12 @@ public class ChangeKdfCommandTests
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = user.GetMasterPasswordSalt()
};
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(IdentityResult.Success));
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>())
.Returns(Task.FromResult(IdentityResult.Success));
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Any<string>()).Returns(false);
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
@@ -150,17 +144,79 @@ public class ChangeKdfCommandTests
&& u.KdfParallelism == constantKdf.Parallelism
&& u.Key == "new-wrapped-key"
));
await sutProvider.GetDependency<IUserService>().Received(1).UpdatePasswordHash(user,
authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: true);
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushLogOutAsync(user.Id);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange);
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
public async Task
ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOn_UpdatesUserCorrectlyAndDoesNotLogOut(
SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
var constantKdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 5,
Memory = 1024,
Parallelism = 4
};
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = constantKdf,
MasterPasswordAuthenticationHash = "new-auth-hash",
Salt = user.GetMasterPasswordSalt()
};
var unlockData = new MasterPasswordUnlockData
{
Kdf = constantKdf,
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = user.GetMasterPasswordSalt()
};
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>())
.Returns(Task.FromResult(IdentityResult.Success));
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Any<string>()).Returns(true);
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData);
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u =>
u.Id == user.Id
&& u.Kdf == constantKdf.KdfType
&& u.KdfIterations == constantKdf.Iterations
&& u.KdfMemory == constantKdf.Memory
&& u.KdfParallelism == constantKdf.Parallelism
&& u.Key == "new-wrapped-key"
));
await sutProvider.GetDependency<IUserService>().Received(1).UpdatePasswordHash(user,
authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: false);
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
.PushLogOutAsync(user.Id, false, PushNotificationLogOutReason.KdfChange);
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncSettingsAsync(user.Id);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange);
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException(
SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 },
Kdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 4,
Memory = 512,
Parallelism = 4
},
MasterPasswordAuthenticationHash = "new-auth-hash",
Salt = user.GetMasterPasswordSalt()
};
@@ -176,9 +232,11 @@ public class ChangeKdfCommandTests
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, KdfSettings kdf)
public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user,
KdfSettings kdf)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
var authenticationData = new MasterPasswordAuthenticationData
{
@@ -192,15 +250,17 @@ public class ChangeKdfCommandTests
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = user.GetMasterPasswordSalt()
};
await Assert.ThrowsAsync<ArgumentException>(async () =>
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, KdfSettings kdf)
public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user,
KdfSettings kdf)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
var authenticationData = new MasterPasswordAuthenticationData
{
@@ -214,25 +274,22 @@ public class ChangeKdfCommandTests
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = "different-salt"
};
await Assert.ThrowsAsync<ArgumentException>(async () =>
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
}
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider<ChangeKdfCommand> sutProvider, User user)
public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider<ChangeKdfCommand> sutProvider,
User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
var failedResult = IdentityResult.Failed(new IdentityError { Code = "TestFail", Description = "Test fail" });
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(failedResult));
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(failedResult));
var kdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 4,
Memory = 512,
Parallelism = 4
};
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
@@ -253,9 +310,11 @@ public class ChangeKdfCommandTests
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(
SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
// Create invalid KDF settings (iterations too low for PBKDF2)
var invalidKdf = new KdfSettings
@@ -287,9 +346,11 @@ public class ChangeKdfCommandTests
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(
SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
// Create invalid Argon2 KDF settings (memory too high)
var invalidKdf = new KdfSettings
@@ -318,5 +379,4 @@ public class ChangeKdfCommandTests
Assert.Equal("KDF settings are invalid.", exception.Message);
}
}

View File

@@ -0,0 +1,43 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Queries;
using Bit.Core.KeyManagement.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.KeyManagement.Queries;
[SutProviderCustomize]
public class UserAccountKeysQueryTests
{
[Theory, BitAutoData]
public async Task V1User_Success(SutProvider<UserAccountKeysQuery> sutProvider, User user)
{
var result = await sutProvider.Sut.Run(user);
Assert.Equal(user.GetPublicKeyEncryptionKeyPair().PublicKey, result.PublicKeyEncryptionKeyPairData.PublicKey);
Assert.Equal(user.GetPublicKeyEncryptionKeyPair().WrappedPrivateKey, result.PublicKeyEncryptionKeyPairData.WrappedPrivateKey);
}
[Theory, BitAutoData]
public async Task V2User_Success(SutProvider<UserAccountKeysQuery> sutProvider, User user)
{
user.SecurityState = "v2";
user.SecurityVersion = 2;
var signatureKeyPairRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
signatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519, "wrappedSigningKey", "verifyingKey"));
var result = await sutProvider.Sut.Run(user);
Assert.Equal(user.GetPublicKeyEncryptionKeyPair().PublicKey, result.PublicKeyEncryptionKeyPairData.PublicKey);
Assert.Equal(user.GetPublicKeyEncryptionKeyPair().WrappedPrivateKey, result.PublicKeyEncryptionKeyPairData.WrappedPrivateKey);
Assert.Equal(user.GetPublicKeyEncryptionKeyPair().SignedPublicKey, result.PublicKeyEncryptionKeyPairData.SignedPublicKey);
Assert.NotNull(result.SignatureKeyPairData);
Assert.Equal("wrappedSigningKey", result.SignatureKeyPairData.WrappedSigningKey);
Assert.Equal("verifyingKey", result.SignatureKeyPairData.VerifyingKey);
Assert.Equal(user.SecurityState, result.SecurityStateData.SecurityState);
Assert.Equal(user.GetSecurityVersion(), result.SecurityStateData.SecurityVersion);
}
}

View File

@@ -1,11 +1,18 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.KeyManagement.UserKey.Implementations;
using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Core.Test.KeyManagement.UserKey;
@@ -14,7 +21,7 @@ namespace Bit.Core.Test.KeyManagement.UserKey;
public class RotateUserAccountKeysCommandTests
{
[Theory, BitAutoData]
public async Task RejectsWrongOldMasterPassword(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
public async Task RotateUserAccountKeysAsync_WrongOldMasterPassword_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
user.Email = model.MasterPasswordUnlockData.Email;
@@ -25,41 +32,38 @@ public class RotateUserAccountKeysCommandTests
Assert.NotEqual(IdentityResult.Success, result);
}
[Theory, BitAutoData]
public async Task ThrowsWhenUserIsNull(SutProvider<RotateUserAccountKeysCommand> sutProvider,
public async Task RotateUserAccountKeysAsync_UserIsNull_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider,
RotateUserAccountKeysData model)
{
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(null, model));
}
[Theory, BitAutoData]
public async Task RejectsEmailChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
public async Task RotateUserAccountKeysAsync_EmailChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
user.Kdf = Enums.KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.MasterPasswordUnlockData.Email = user.Email + ".different-domain";
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
model.MasterPasswordUnlockData.KdfIterations = 3;
model.MasterPasswordUnlockData.KdfMemory = 64;
model.MasterPasswordUnlockData.KdfParallelism = 4;
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
.Returns(true);
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
}
[Theory, BitAutoData]
public async Task RejectsKdfChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
public async Task RotateUserAccountKeysAsync_KdfChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
user.Kdf = Enums.KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.MasterPasswordUnlockData.Email = user.Email;
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.PBKDF2_SHA256;
model.MasterPasswordUnlockData.KdfIterations = 600000;
model.MasterPasswordUnlockData.KdfMemory = null;
@@ -71,22 +75,15 @@ public class RotateUserAccountKeysCommandTests
[Theory, BitAutoData]
public async Task RejectsPublicKeyChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
public async Task RotateUserAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
user.PublicKey = "old-public";
user.Kdf = Enums.KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
model.AccountPublicKey = "new-public";
model.MasterPasswordUnlockData.Email = user.Email;
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
model.MasterPasswordUnlockData.KdfIterations = 3;
model.MasterPasswordUnlockData.KdfMemory = 64;
model.MasterPasswordUnlockData.KdfParallelism = 4;
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public";
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
.Returns(true);
@@ -94,27 +91,350 @@ public class RotateUserAccountKeysCommandTests
}
[Theory, BitAutoData]
public async Task RotatesCorrectly(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
public async Task RotateUserAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
user.Kdf = Enums.KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
model.MasterPasswordUnlockData.Email = user.Email;
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
model.MasterPasswordUnlockData.KdfIterations = 3;
model.MasterPasswordUnlockData.KdfMemory = 64;
model.MasterPasswordUnlockData.KdfParallelism = 4;
model.AccountPublicKey = user.PublicKey;
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
.Returns(true);
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
Assert.Equal(IdentityResult.Success, result);
}
[Theory, BitAutoData]
public async Task RotateUserAccountKeysAsync_UpgradeV1ToV2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
.Returns(true);
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
Assert.Equal(IdentityResult.Success, result);
Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V2User_PrivateKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "2.xxx";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V1User_PrivateKeyNotAesCbcHmac_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "7.xxx";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("The provided account private key was not wrapped with AES-256-CBC-HMAC", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
Assert.Empty(saveEncryptedDataActions);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
Assert.NotEmpty(saveEncryptedDataActions);
Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V2User_VerifyingKeyMismatch_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.SignatureKeyPairData.VerifyingKey = "different-verifying-key";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("The provided verifying key does not match the user's current verifying key.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V2User_SignedPublicKeyNullOrEmpty_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null;
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V2User_WrappedSigningKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.SignatureKeyPairData.WrappedSigningKey = "2.xxx";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("The provided signing key data is not wrapped with XChaCha20-Poly1305.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeys_UpgradeToV2_InvalidVerifyingKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.SignatureKeyPairData.VerifyingKey = "";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("The provided signature key pair data does not contain a valid verifying key.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_UpgradeToV2_IncorrectlyWrappedPrivateKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "2.abc";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("The provided private key encryption key is not wrapped with XChaCha20-Poly1305.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSignedPublicKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null;
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSecurityState_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.SecurityStateData = null;
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("No signed security state provider for V2 user", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_RotateV2_NoSignatureKeyPair_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.SignatureKeyPairData = null;
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("Signature key pair data is required for V2 encryption.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_GetEncryptionType_EmptyString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("Invalid encryption type string.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_GetEncryptionType_InvalidString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "9.xxx";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("Invalid encryption type string.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateUserData_RevisionDateChanged_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
var oldDate = new DateTime(2017, 1, 1);
var cipher = Substitute.For<Cipher>();
cipher.RevisionDate = oldDate;
model.Ciphers = [cipher];
var folder = Substitute.For<Folder>();
folder.RevisionDate = oldDate;
model.Folders = [folder];
var send = Substitute.For<Send>();
send.RevisionDate = oldDate;
model.Sends = [send];
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
sutProvider.Sut.UpdateUserData(model, user, saveEncryptedDataActions);
foreach (var dataAction in saveEncryptedDataActions)
{
await dataAction.Invoke();
}
var updatedCiphers = sutProvider.GetDependency<ICipherRepository>()
.ReceivedCalls()
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")?
.GetArguments()[1] as IEnumerable<Cipher>;
foreach (var updatedCipher in updatedCiphers!)
{
var oldCipher = model.Ciphers.FirstOrDefault(c => c.Id == updatedCipher.Id);
Assert.NotEqual(oldDate, updatedCipher.RevisionDate);
}
var updatedFolders = sutProvider.GetDependency<IFolderRepository>()
.ReceivedCalls()
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")?
.GetArguments()[1] as IEnumerable<Folder>;
foreach (var updatedFolder in updatedFolders!)
{
var oldFolder = model.Folders.FirstOrDefault(f => f.Id == updatedFolder.Id);
Assert.NotEqual(oldDate, updatedFolder.RevisionDate);
}
var updatedSends = sutProvider.GetDependency<ISendRepository>()
.ReceivedCalls()
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")?
.GetArguments()[1] as IEnumerable<Send>;
foreach (var updatedSend in updatedSends!)
{
var oldSend = model.Sends.FirstOrDefault(s => s.Id == updatedSend.Id);
Assert.NotEqual(oldDate, updatedSend.RevisionDate);
}
}
// Helper functions to set valid test parameters that match each other to the model and user.
private static void SetTestKdfAndSaltForUserAndModel(User user, RotateUserAccountKeysData model)
{
user.Kdf = Enums.KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
model.MasterPasswordUnlockData.KdfIterations = 3;
model.MasterPasswordUnlockData.KdfMemory = 64;
model.MasterPasswordUnlockData.KdfParallelism = 4;
// The email is the salt for the KDF and is validated currently.
user.Email = model.MasterPasswordUnlockData.Email;
}
private static void SetV1ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
{
user.PrivateKey = "2.abc";
user.PublicKey = "public";
user.SignedPublicKey = null;
userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).ReturnsNull();
}
private static void SetV2ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
{
user.PrivateKey = "7.abc";
user.PublicKey = "public";
user.SignedPublicKey = "signed-public";
userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "7.abc", "verifying-key"));
}
private static void SetV1ModelUser(RotateUserAccountKeysData model)
{
model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("2.abc", "public", null);
model.AccountKeys.SignatureKeyPairData = null;
model.AccountKeys.SecurityStateData = null;
}
private static void SetV2ModelUser(RotateUserAccountKeysData model)
{
model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("7.abc", "public", "signed-public");
model.AccountKeys.SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "7.abc", "verifying-key");
model.AccountKeys.SecurityStateData = new SecurityStateData
{
SecurityState = "abc",
SecurityVersion = 2,
};
}
}

View File

@@ -358,20 +358,28 @@ public class AzureQueuePushEngineTests
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext)
[InlineData(true, null)]
[InlineData(true, PushNotificationLogOutReason.KdfChange)]
[InlineData(false, null)]
[InlineData(false, PushNotificationLogOutReason.KdfChange)]
public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext,
PushNotificationLogOutReason? reason)
{
var userId = Guid.NewGuid();
var payload = new JsonObject
{
["UserId"] = userId
};
if (reason != null)
{
payload["Reason"] = (int)reason;
}
var expectedPayload = new JsonObject
{
["Type"] = 11,
["Payload"] = new JsonObject
{
["UserId"] = userId,
["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime,
},
["Payload"] = payload,
};
if (excludeCurrentContext)
@@ -380,7 +388,7 @@ public class AzureQueuePushEngineTests
}
await VerifyNotificationAsync(
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext),
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason),
expectedPayload
);
}

View File

@@ -1,6 +1,7 @@
using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Tools.Entities;
@@ -193,7 +194,8 @@ public class NotificationsApiPushEngineTests : PushTestBase
};
}
protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext)
protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext,
PushNotificationLogOutReason? reason)
{
JsonNode? contextId = excludeCurrentContext ? DeviceIdentifier : null;
@@ -203,7 +205,7 @@ public class NotificationsApiPushEngineTests : PushTestBase
["Payload"] = new JsonObject
{
["UserId"] = userId,
["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime,
["Reason"] = reason != null ? (int)reason : null
},
["ContextId"] = contextId,
};

View File

@@ -86,7 +86,8 @@ public abstract class PushTestBase
protected abstract JsonNode GetPushSyncOrganizationsPayload(Guid userId);
protected abstract JsonNode GetPushSyncOrgKeysPayload(Guid userId);
protected abstract JsonNode GetPushSyncSettingsPayload(Guid userId);
protected abstract JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext);
protected abstract JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext,
PushNotificationLogOutReason? reason);
protected abstract JsonNode GetPushSendCreatePayload(Send send);
protected abstract JsonNode GetPushSendUpdatePayload(Send send);
protected abstract JsonNode GetPushSendDeletePayload(Send send);
@@ -263,15 +264,18 @@ public abstract class PushTestBase
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext)
[InlineData(true, null)]
[InlineData(true, PushNotificationLogOutReason.KdfChange)]
[InlineData(false, null)]
[InlineData(false, PushNotificationLogOutReason.KdfChange)]
public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext,
PushNotificationLogOutReason? reason)
{
var userId = Guid.NewGuid();
await VerifyNotificationAsync(
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext),
GetPushLogOutPayload(userId, excludeCurrentContext)
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason),
GetPushLogOutPayload(userId, excludeCurrentContext, reason)
);
}

View File

@@ -4,6 +4,7 @@ using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Repositories;
@@ -64,7 +65,7 @@ public class RelayPushNotificationServiceTests : PushTestBase
["UserId"] = cipher.UserId,
["OrganizationId"] = null,
// Currently CollectionIds are not passed along from the method signature
// to the request body.
// to the request body.
["CollectionIds"] = null,
["RevisionDate"] = cipher.RevisionDate,
},
@@ -88,7 +89,7 @@ public class RelayPushNotificationServiceTests : PushTestBase
["UserId"] = cipher.UserId,
["OrganizationId"] = null,
// Currently CollectionIds are not passed along from the method signature
// to the request body.
// to the request body.
["CollectionIds"] = null,
["RevisionDate"] = cipher.RevisionDate,
},
@@ -274,7 +275,8 @@ public class RelayPushNotificationServiceTests : PushTestBase
};
}
protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext)
protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext,
PushNotificationLogOutReason? reason)
{
JsonNode? identifier = excludeCurrentContext ? DeviceIdentifier : null;
@@ -288,7 +290,7 @@ public class RelayPushNotificationServiceTests : PushTestBase
["Payload"] = new JsonObject
{
["UserId"] = userId,
["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime,
["Reason"] = reason != null ? (int)reason : null
},
["ClientType"] = null,
["InstallationId"] = null,

View File

@@ -404,16 +404,18 @@ public class NotificationHubPushNotificationServiceTests
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task PushLogOutAsync_SendExpectedData(bool excludeCurrentContext)
[InlineData(true, null)]
[InlineData(true, PushNotificationLogOutReason.KdfChange)]
[InlineData(false, null)]
[InlineData(false, PushNotificationLogOutReason.KdfChange)]
public async Task PushLogOutAsync_SendExpectedData(bool excludeCurrentContext, PushNotificationLogOutReason? reason)
{
var userId = Guid.NewGuid();
var expectedPayload = new JsonObject
{
["UserId"] = userId,
["Date"] = _now,
["Reason"] = reason != null ? (int)reason : null,
};
var expectedTag = excludeCurrentContext
@@ -421,7 +423,7 @@ public class NotificationHubPushNotificationServiceTests
: $"(template:payload_userId:{userId})";
await VerifyNotificationAsync(
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext),
async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason),
PushType.LogOut,
expectedPayload,
expectedTag

View File

@@ -18,8 +18,9 @@ public class StripePaymentServiceTests
{
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
public async Task
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
@@ -28,16 +29,13 @@ public class StripePaymentServiceTests
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually,
AdditionalStorage = 0
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
PasswordManager =
new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually,
AdditionalStorage = 0
},
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
};
sutProvider.GetDependency<IStripeAdapter>()
@@ -52,7 +50,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 4000,
Tax = 800,
TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],
Total = 4800
});
@@ -75,16 +73,13 @@ public class StripePaymentServiceTests
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually,
AdditionalStorage = 1
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
PasswordManager =
new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually,
AdditionalStorage = 1
},
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
};
sutProvider.GetDependency<IStripeAdapter>()
@@ -96,12 +91,7 @@ public class StripePaymentServiceTests
p.SubscriptionDetails.Items.Any(x =>
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
x.Quantity == 1)))
.Returns(new Invoice
{
TotalExcludingTax = 4000,
Tax = 800,
Total = 4800
});
.Returns(new Invoice { TotalExcludingTax = 4000, TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 4800 });
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
@@ -112,8 +102,9 @@ public class StripePaymentServiceTests
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
public async Task
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
@@ -128,11 +119,7 @@ public class StripePaymentServiceTests
SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise,
AdditionalStorage = 0
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
};
sutProvider.GetDependency<IStripeAdapter>()
@@ -144,12 +131,7 @@ public class StripePaymentServiceTests
p.SubscriptionDetails.Items.Any(x =>
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
x.Quantity == 0)))
.Returns(new Invoice
{
TotalExcludingTax = 0,
Tax = 0,
Total = 0
});
.Returns(new Invoice { TotalExcludingTax = 0, TotalTaxes = [new InvoiceTotalTax { Amount = 0 }], Total = 0 });
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
@@ -160,8 +142,9 @@ public class StripePaymentServiceTests
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
public async Task
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
@@ -176,11 +159,7 @@ public class StripePaymentServiceTests
SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise,
AdditionalStorage = 1
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
};
sutProvider.GetDependency<IStripeAdapter>()
@@ -192,12 +171,7 @@ public class StripePaymentServiceTests
p.SubscriptionDetails.Items.Any(x =>
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
x.Quantity == 1)))
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
.Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 });
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
@@ -235,7 +209,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -277,7 +251,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -319,7 +293,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -361,7 +335,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -403,7 +377,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -445,7 +419,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -487,7 +461,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});
@@ -529,7 +503,7 @@ public class StripePaymentServiceTests
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
Total = 408
});