1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 23:52:50 -05:00

Merge branch 'main' into jmccannon/ac/pm-16811-scim-invite-optimization

# Conflicts:
#	src/Core/Models/Commands/CommandResult.cs
This commit is contained in:
jrmccannon
2025-03-17 08:59:17 -05:00
77 changed files with 11892 additions and 789 deletions

View File

@ -0,0 +1,23 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
/// <summary>
/// Intentionally simplified PolicyRequirement that just holds the input PolicyDetails for us to assert against.
/// </summary>
public class TestPolicyRequirement : IPolicyRequirement
{
public IEnumerable<PolicyDetails> Policies { get; init; } = [];
}
public class TestPolicyRequirementFactory(Func<PolicyDetails, bool> enforce) : IPolicyRequirementFactory<TestPolicyRequirement>
{
public PolicyType PolicyType => PolicyType.SingleOrg;
public bool Enforce(PolicyDetails policyDetails) => enforce(policyDetails);
public TestPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
=> new() { Policies = policyDetails };
}

View File

@ -1,6 +1,6 @@
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -11,50 +11,72 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
[SutProviderCustomize]
public class PolicyRequirementQueryTests
{
/// <summary>
/// Tests that the query correctly registers, retrieves and instantiates arbitrary IPolicyRequirements
/// according to their provided CreateRequirement delegate.
/// </summary>
[Theory, BitAutoData]
public async Task GetAsync_Works(Guid userId, Guid organizationId)
public async Task GetAsync_IgnoresOtherPolicyTypes(Guid userId)
{
var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };
var otherPolicy = new PolicyDetails { PolicyType = PolicyType.RequireSso };
var policyRepository = Substitute.For<IPolicyRepository>();
var factories = new List<RequirementFactory<IPolicyRequirement>>
{
// In prod this cast is handled when the CreateRequirement delegate is registered in DI
(RequirementFactory<TestPolicyRequirement>)TestPolicyRequirement.Create
};
policyRepository.GetPolicyDetailsByUserId(userId).Returns([otherPolicy, thisPolicy]);
var sut = new PolicyRequirementQuery(policyRepository, factories);
policyRepository.GetPolicyDetailsByUserId(userId).Returns([
new PolicyDetails
{
OrganizationId = organizationId
}
]);
var factory = new TestPolicyRequirementFactory(_ => true);
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
var requirement = await sut.GetAsync<TestPolicyRequirement>(userId);
Assert.Equal(organizationId, requirement.OrganizationId);
Assert.Contains(thisPolicy, requirement.Policies);
Assert.DoesNotContain(otherPolicy, requirement.Policies);
}
[Theory, BitAutoData]
public async Task GetAsync_ThrowsIfNoRequirementRegistered(Guid userId)
public async Task GetAsync_CallsEnforceCallback(Guid userId)
{
// 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]);
// Arrange a substitute Enforce function so that we can inspect the received calls
var callback = Substitute.For<Func<PolicyDetails, bool>>();
callback(Arg.Any<PolicyDetails>()).Returns(x => x.Arg<PolicyDetails>() == thisPolicy);
// Arrange the sut
var factory = new TestPolicyRequirementFactory(callback);
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
// Act
var requirement = await sut.GetAsync<TestPolicyRequirement>(userId);
// Assert
Assert.Contains(thisPolicy, requirement.Policies);
Assert.DoesNotContain(otherPolicy, requirement.Policies);
callback.Received()(Arg.Is(thisPolicy));
callback.Received()(Arg.Is(otherPolicy));
}
[Theory, BitAutoData]
public async Task GetAsync_ThrowsIfNoFactoryRegistered(Guid userId)
{
var policyRepository = Substitute.For<IPolicyRepository>();
var sut = new PolicyRequirementQuery(policyRepository, []);
var exception = await Assert.ThrowsAsync<NotImplementedException>(()
=> sut.GetAsync<TestPolicyRequirement>(userId));
Assert.Contains("No Policy Requirement found", exception.Message);
Assert.Contains("No Requirement Factory found", exception.Message);
}
/// <summary>
/// Intentionally simplified PolicyRequirement that just holds the Policy.OrganizationId for us to assert against.
/// </summary>
private class TestPolicyRequirement : IPolicyRequirement
[Theory, BitAutoData]
public async Task GetAsync_HandlesNoPolicies(Guid userId)
{
public Guid OrganizationId { get; init; }
public static TestPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
=> new() { OrganizationId = policyDetails.Single().OrganizationId };
var policyRepository = Substitute.For<IPolicyRepository>();
policyRepository.GetPolicyDetailsByUserId(userId).Returns([]);
var factory = new TestPolicyRequirementFactory(x => x.IsProvider);
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
var requirement = await sut.GetAsync<TestPolicyRequirement>(userId);
Assert.Empty(requirement.Policies);
}
}

View File

@ -0,0 +1,90 @@
using AutoFixture.Xunit2;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Enums;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
public class BasePolicyRequirementFactoryTests
{
[Theory, AutoData]
public void ExemptRoles_DoesNotEnforceAgainstThoseRoles(
[PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Owner)] PolicyDetails ownerPolicy,
[PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Admin)] PolicyDetails adminPolicy,
[PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Custom)] PolicyDetails customPolicy,
[PolicyDetails(PolicyType.SingleOrg)] PolicyDetails userPolicy)
{
var sut = new TestPolicyRequirementFactory(
// These exempt roles are intentionally unusual to make sure we're properly testing the sut
[OrganizationUserType.User, OrganizationUserType.Custom],
[],
false);
Assert.True(sut.Enforce(ownerPolicy));
Assert.True(sut.Enforce(adminPolicy));
Assert.False(sut.Enforce(customPolicy));
Assert.False(sut.Enforce(userPolicy));
}
[Theory, AutoData]
public void ExemptStatuses_DoesNotEnforceAgainstThoseStatuses(
[PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Invited)] PolicyDetails invitedPolicy,
[PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Accepted)] PolicyDetails acceptedPolicy,
[PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails confirmedPolicy,
[PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Revoked)] PolicyDetails revokedPolicy)
{
var sut = new TestPolicyRequirementFactory(
[],
// These exempt statuses are intentionally unusual to make sure we're properly testing the sut
[OrganizationUserStatusType.Confirmed, OrganizationUserStatusType.Accepted],
false);
Assert.True(sut.Enforce(invitedPolicy));
Assert.True(sut.Enforce(revokedPolicy));
Assert.False(sut.Enforce(confirmedPolicy));
Assert.False(sut.Enforce(acceptedPolicy));
}
[Theory, AutoData]
public void ExemptProviders_DoesNotEnforceAgainstProviders(
[PolicyDetails(PolicyType.SingleOrg, isProvider: true)] PolicyDetails policy)
{
var sut = new TestPolicyRequirementFactory(
[],
[],
true);
Assert.False(sut.Enforce(policy));
}
[Theory, AutoData]
public void NoExemptions_EnforcesAgainstAdminsAndProviders(
[PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Owner, isProvider: true)] PolicyDetails policy)
{
var sut = new TestPolicyRequirementFactory(
[],
[],
false);
Assert.True(sut.Enforce(policy));
}
private class TestPolicyRequirementFactory(
IEnumerable<OrganizationUserType> exemptRoles,
IEnumerable<OrganizationUserStatusType> exemptStatuses,
bool exemptProviders
) : BasePolicyRequirementFactory<TestPolicyRequirement>
{
public override PolicyType PolicyType => PolicyType.SingleOrg;
protected override IEnumerable<OrganizationUserType> ExemptRoles => exemptRoles;
protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => exemptStatuses;
protected override bool ExemptProviders => exemptProviders;
public override TestPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
=> new() { Policies = policyDetails };
}
}

View File

@ -0,0 +1,32 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
[SutProviderCustomize]
public class DisableSendPolicyRequirementFactoryTests
{
[Theory, BitAutoData]
public void DisableSend_IsFalse_IfNoPolicies(SutProvider<DisableSendPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create([]);
Assert.False(actual.DisableSend);
}
[Theory, BitAutoData]
public void DisableSend_IsTrue_IfAnyDisableSendPolicies(
[PolicyDetails(PolicyType.DisableSend)] PolicyDetails[] policies,
SutProvider<DisableSendPolicyRequirementFactory> sutProvider
)
{
var actual = sutProvider.Sut.Create(policies);
Assert.True(actual.DisableSend);
}
}

View File

@ -0,0 +1,49 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
[SutProviderCustomize]
public class SendOptionsPolicyRequirementFactoryTests
{
[Theory, BitAutoData]
public void DisableHideEmail_IsFalse_IfNoPolicies(SutProvider<SendOptionsPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create([]);
Assert.False(actual.DisableHideEmail);
}
[Theory, BitAutoData]
public void DisableHideEmail_IsFalse_IfNotConfigured(
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies,
SutProvider<SendOptionsPolicyRequirementFactory> sutProvider
)
{
policies[0].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false });
policies[1].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false });
var actual = sutProvider.Sut.Create(policies);
Assert.False(actual.DisableHideEmail);
}
[Theory, BitAutoData]
public void DisableHideEmail_IsTrue_IfAnyConfigured(
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies,
SutProvider<SendOptionsPolicyRequirementFactory> sutProvider
)
{
policies[0].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true });
policies[1].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false });
var actual = sutProvider.Sut.Create(policies);
Assert.True(actual.DisableHideEmail);
}
}

View File

@ -1,138 +0,0 @@
using AutoFixture.Xunit2;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Enums;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
public class SendPolicyRequirementTests
{
[Theory, AutoData]
public void DisableSend_IsFalse_IfNoDisableSendPolicies(
[PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1,
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails otherPolicy2)
{
EnableDisableHideEmail(otherPolicy2);
var actual = SendPolicyRequirement.Create([otherPolicy1, otherPolicy2]);
Assert.False(actual.DisableSend);
}
[Theory]
[InlineAutoData(OrganizationUserType.Owner, false)]
[InlineAutoData(OrganizationUserType.Admin, false)]
[InlineAutoData(OrganizationUserType.User, true)]
[InlineAutoData(OrganizationUserType.Custom, true)]
public void DisableSend_TestRoles(
OrganizationUserType userType,
bool shouldBeEnforced,
[PolicyDetails(PolicyType.DisableSend)] PolicyDetails policyDetails)
{
policyDetails.OrganizationUserType = userType;
var actual = SendPolicyRequirement.Create([policyDetails]);
Assert.Equal(shouldBeEnforced, actual.DisableSend);
}
[Theory, AutoData]
public void DisableSend_Not_EnforcedAgainstProviders(
[PolicyDetails(PolicyType.DisableSend, isProvider: true)] PolicyDetails policyDetails)
{
var actual = SendPolicyRequirement.Create([policyDetails]);
Assert.False(actual.DisableSend);
}
[Theory]
[InlineAutoData(OrganizationUserStatusType.Confirmed, true)]
[InlineAutoData(OrganizationUserStatusType.Accepted, true)]
[InlineAutoData(OrganizationUserStatusType.Invited, false)]
[InlineAutoData(OrganizationUserStatusType.Revoked, false)]
public void DisableSend_TestStatuses(
OrganizationUserStatusType userStatus,
bool shouldBeEnforced,
[PolicyDetails(PolicyType.DisableSend)] PolicyDetails policyDetails)
{
policyDetails.OrganizationUserStatus = userStatus;
var actual = SendPolicyRequirement.Create([policyDetails]);
Assert.Equal(shouldBeEnforced, actual.DisableSend);
}
[Theory, AutoData]
public void DisableHideEmail_IsFalse_IfNoSendOptionsPolicies(
[PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1,
[PolicyDetails(PolicyType.DisableSend)] PolicyDetails otherPolicy2)
{
var actual = SendPolicyRequirement.Create([otherPolicy1, otherPolicy2]);
Assert.False(actual.DisableHideEmail);
}
[Theory]
[InlineAutoData(OrganizationUserType.Owner, false)]
[InlineAutoData(OrganizationUserType.Admin, false)]
[InlineAutoData(OrganizationUserType.User, true)]
[InlineAutoData(OrganizationUserType.Custom, true)]
public void DisableHideEmail_TestRoles(
OrganizationUserType userType,
bool shouldBeEnforced,
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails)
{
EnableDisableHideEmail(policyDetails);
policyDetails.OrganizationUserType = userType;
var actual = SendPolicyRequirement.Create([policyDetails]);
Assert.Equal(shouldBeEnforced, actual.DisableHideEmail);
}
[Theory, AutoData]
public void DisableHideEmail_Not_EnforcedAgainstProviders(
[PolicyDetails(PolicyType.SendOptions, isProvider: true)] PolicyDetails policyDetails)
{
EnableDisableHideEmail(policyDetails);
var actual = SendPolicyRequirement.Create([policyDetails]);
Assert.False(actual.DisableHideEmail);
}
[Theory]
[InlineAutoData(OrganizationUserStatusType.Confirmed, true)]
[InlineAutoData(OrganizationUserStatusType.Accepted, true)]
[InlineAutoData(OrganizationUserStatusType.Invited, false)]
[InlineAutoData(OrganizationUserStatusType.Revoked, false)]
public void DisableHideEmail_TestStatuses(
OrganizationUserStatusType userStatus,
bool shouldBeEnforced,
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails)
{
EnableDisableHideEmail(policyDetails);
policyDetails.OrganizationUserStatus = userStatus;
var actual = SendPolicyRequirement.Create([policyDetails]);
Assert.Equal(shouldBeEnforced, actual.DisableHideEmail);
}
[Theory, AutoData]
public void DisableHideEmail_HandlesNullData(
[PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails)
{
policyDetails.PolicyData = null;
var actual = SendPolicyRequirement.Create([policyDetails]);
Assert.False(actual.DisableHideEmail);
}
private static void EnableDisableHideEmail(PolicyDetails policyDetails)
=> policyDetails.SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true });
}

View File

@ -0,0 +1,58 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.AdminConsole.Shared.Validation;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Shared;
public class IValidatorTests
{
public class TestClass
{
public string Name { get; set; } = string.Empty;
}
public record InvalidRequestError<T>(T ErroredValue) : Error<T>(Code, ErroredValue)
{
public const string Code = "InvalidRequest";
}
public class TestClassValidator : IValidator<TestClass>
{
public Task<ValidationResult<TestClass>> ValidateAsync(TestClass value)
{
if (string.IsNullOrWhiteSpace(value.Name))
{
return Task.FromResult<ValidationResult<TestClass>>(new Invalid<TestClass>
{
Errors = [new InvalidRequestError<TestClass>(value)]
});
}
return Task.FromResult<ValidationResult<TestClass>>(new Valid<TestClass> { Value = value });
}
}
[Fact]
public async Task ValidateAsync_WhenSomethingIsInvalid_ReturnsInvalidWithError()
{
var example = new TestClass();
var result = await new TestClassValidator().ValidateAsync(example);
Assert.IsType<Invalid<TestClass>>(result);
var invalidResult = result as Invalid<TestClass>;
Assert.Equal(InvalidRequestError<TestClass>.Code, invalidResult.Errors.First().Message);
}
[Fact]
public async Task ValidateAsync_WhenIsValid_ReturnsValid()
{
var example = new TestClass { Name = "Valid" };
var result = await new TestClassValidator().ValidateAsync(example);
Assert.IsType<Valid<TestClass>>(result);
var validResult = result as Valid<TestClass>;
Assert.Equal(example.Name, validResult.Value.Name);
}
}

View File

@ -0,0 +1,53 @@
using Bit.Core.AdminConsole.Errors;
using Bit.Core.Models.Commands;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Models.Commands;
public class CommandResultTests
{
public class TestItem
{
public Guid Id { get; set; }
public string Value { get; set; }
}
public CommandResult<TestItem> BulkAction(IEnumerable<TestItem> items)
{
var itemList = items.ToList();
var successfulItems = items.Where(x => x.Value == "SuccessfulRequest").ToArray();
var failedItems = itemList.Except(successfulItems).ToArray();
var notFound = failedItems.First(x => x.Value == "Failed due to not found");
var invalidPermissions = failedItems.First(x => x.Value == "Failed due to invalid permissions");
var notFoundError = new RecordNotFoundError<TestItem>(notFound);
var insufficientPermissionsError = new InsufficientPermissionsError<TestItem>(invalidPermissions);
return new Partial<TestItem>(successfulItems.ToArray(), [notFoundError, insufficientPermissionsError]);
}
[Theory]
[BitAutoData]
public void Partial_CommandResult_BulkRequestWithSuccessAndFailures(Guid successId1, Guid failureId1, Guid failureId2)
{
var listOfRecords = new List<TestItem>
{
new TestItem() { Id = successId1, Value = "SuccessfulRequest" },
new TestItem() { Id = failureId1, Value = "Failed due to not found" },
new TestItem() { Id = failureId2, Value = "Failed due to invalid permissions" }
};
var result = BulkAction(listOfRecords);
Assert.IsType<Partial<TestItem>>(result);
var failures = (result as Partial<TestItem>).Failures.ToArray();
var success = (result as Partial<TestItem>).Successes.First();
Assert.Equal(listOfRecords.First(), success);
Assert.Equal(2, failures.Length);
}
}

View File

@ -56,7 +56,6 @@ public class CloudGetOrganizationLicenseQueryTests
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor).Returns(false);
var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);
@ -64,7 +63,7 @@ public class CloudGetOrganizationLicenseQueryTests
Assert.Equal(organization.Id, result.Id);
Assert.Equal(installationId, result.InstallationId);
Assert.Equal(licenseSignature, result.SignatureBytes);
Assert.Null(result.Token);
Assert.Equal(string.Empty, result.Token);
}
[Theory]
@ -77,7 +76,6 @@ public class CloudGetOrganizationLicenseQueryTests
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor).Returns(true);
sutProvider.GetDependency<ILicensingService>()
.CreateOrganizationTokenAsync(organization, installationId, subInfo)
.Returns(token);

View File

@ -123,10 +123,12 @@ public class SendServiceTests
// Disable Send policy check - vNext
private void SaveSendAsync_Setup_vNext(SutProvider<SendService> sutProvider, Send send,
SendPolicyRequirement sendPolicyRequirement)
DisableSendPolicyRequirement disableSendPolicyRequirement, SendOptionsPolicyRequirement sendOptionsPolicyRequirement)
{
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<SendPolicyRequirement>(send.UserId!.Value)
.Returns(sendPolicyRequirement);
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<DisableSendPolicyRequirement>(send.UserId!.Value)
.Returns(disableSendPolicyRequirement);
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<SendOptionsPolicyRequirement>(send.UserId!.Value)
.Returns(sendOptionsPolicyRequirement);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
// Should not be called in these tests
@ -141,7 +143,7 @@ public class SendServiceTests
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
{
send.Type = sendType;
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableSend = true });
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement { DisableSend = true }, new SendOptionsPolicyRequirement());
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send));
Assert.Contains("Due to an Enterprise Policy, you are only able to delete an existing Send.",
@ -155,7 +157,7 @@ public class SendServiceTests
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
{
send.Type = sendType;
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement());
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement());
await sutProvider.Sut.SaveSendAsync(send);
@ -171,7 +173,7 @@ public class SendServiceTests
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
{
send.Type = sendType;
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true });
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true });
send.HideEmail = true;
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveSendAsync(send));
@ -185,7 +187,7 @@ public class SendServiceTests
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
{
send.Type = sendType;
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true });
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true });
send.HideEmail = false;
await sutProvider.Sut.SaveSendAsync(send);
@ -200,7 +202,7 @@ public class SendServiceTests
SutProvider<SendService> sutProvider, [NewUserSendCustomize] Send send)
{
send.Type = sendType;
SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement());
SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement());
send.HideEmail = true;
await sutProvider.Sut.SaveSendAsync(send);

View File

@ -602,6 +602,78 @@ public class CipherServiceTests
Assert.NotEqual(initialRevisionDate, cipher.RevisionDate);
}
[Theory]
[BitAutoData]
public async Task RestoreAsync_WithAlreadyRestoredCipher_SkipsOperation(
Guid restoringUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
cipher.DeletedDate = null;
await sutProvider.Sut.RestoreAsync(cipher, restoringUserId, true);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().UpsertAsync(default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
}
[Theory]
[BitAutoData]
public async Task RestoreAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException(
Guid restoringUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
cipher.UserId = Guid.NewGuid();
cipher.OrganizationId = null;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreAsync(cipher, restoringUserId));
Assert.Contains("do not have permissions", exception.Message);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().UpsertAsync(default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
}
[Theory]
[OrganizationCipherCustomize]
[BitAutoData]
public async Task RestoreAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
Guid restoringUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
sutProvider.GetDependency<ICipherRepository>()
.GetCanEditByIdAsync(restoringUserId, cipher.Id)
.Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreAsync(cipher, restoringUserId));
Assert.Contains("do not have permissions", exception.Message);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().UpsertAsync(default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default);
}
[Theory]
[BitAutoData]
public async Task RestoreAsync_WithCipherDetailsType_RestoresCipherDetails(
Guid restoringUserId, CipherDetails cipherDetails, SutProvider<CipherService> sutProvider)
{
sutProvider.GetDependency<ICipherRepository>()
.GetCanEditByIdAsync(restoringUserId, cipherDetails.Id)
.Returns(true);
var initialRevisionDate = new DateTime(1970, 1, 1, 0, 0, 0);
cipherDetails.DeletedDate = initialRevisionDate;
cipherDetails.RevisionDate = initialRevisionDate;
await sutProvider.Sut.RestoreAsync(cipherDetails, restoringUserId);
Assert.Null(cipherDetails.DeletedDate);
Assert.NotEqual(initialRevisionDate, cipherDetails.RevisionDate);
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipherDetails);
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipherDetails, EventType.Cipher_Restored);
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipherDetails, null);
}
[Theory]
[BitAutoData]
public async Task RestoreManyAsync_UpdatesCiphers(ICollection<CipherDetails> ciphers,
@ -725,6 +797,188 @@ public class CipherServiceTests
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}
[Theory]
[BitAutoData]
public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher(
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
cipher.UserId = deletingUserId;
cipher.OrganizationId = null;
await sutProvider.Sut.DeleteAsync(cipher, deletingUserId);
await sutProvider.GetDependency<ICipherRepository>().Received(1).DeleteAsync(cipher);
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1).DeleteAttachmentsForCipherAsync(cipher.Id);
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_Deleted);
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherDeleteAsync(cipher);
}
[Theory]
[OrganizationCipherCustomize]
[BitAutoData]
public async Task DeleteAsync_WithOrgCipherAndEditPermission_DeletesCipher(
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
sutProvider.GetDependency<ICipherRepository>()
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
.Returns(true);
await sutProvider.Sut.DeleteAsync(cipher, deletingUserId);
await sutProvider.GetDependency<ICipherRepository>().Received(1).DeleteAsync(cipher);
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1).DeleteAttachmentsForCipherAsync(cipher.Id);
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_Deleted);
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherDeleteAsync(cipher);
}
[Theory]
[BitAutoData]
public async Task DeleteAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException(
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
cipher.UserId = Guid.NewGuid();
cipher.OrganizationId = null;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.DeleteAsync(cipher, deletingUserId));
Assert.Contains("do not have permissions", exception.Message);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().DeleteAsync(default);
await sutProvider.GetDependency<IAttachmentStorageService>().DidNotReceiveWithAnyArgs().DeleteAttachmentsForCipherAsync(default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherDeleteAsync(default);
}
[Theory]
[OrganizationCipherCustomize]
[BitAutoData]
public async Task DeleteAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
sutProvider.GetDependency<ICipherRepository>()
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
.Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.DeleteAsync(cipher, deletingUserId));
Assert.Contains("do not have permissions", exception.Message);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().DeleteAsync(default);
await sutProvider.GetDependency<IAttachmentStorageService>().DidNotReceiveWithAnyArgs().DeleteAttachmentsForCipherAsync(default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default);
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushSyncCipherDeleteAsync(default);
}
[Theory]
[BitAutoData]
public async Task SoftDeleteAsync_WithPersonalCipherOwner_SoftDeletesCipher(
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
cipher.UserId = deletingUserId;
cipher.OrganizationId = null;
cipher.DeletedDate = null;
sutProvider.GetDependency<ICipherRepository>()
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
.Returns(true);
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId);
Assert.NotNull(cipher.DeletedDate);
Assert.Equal(cipher.RevisionDate, cipher.DeletedDate);
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipher);
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipher, null);
}
[Theory]
[OrganizationCipherCustomize]
[BitAutoData]
public async Task SoftDeleteAsync_WithOrgCipherAndEditPermission_SoftDeletesCipher(
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
cipher.DeletedDate = null;
sutProvider.GetDependency<ICipherRepository>()
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
.Returns(true);
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId);
Assert.NotNull(cipher.DeletedDate);
Assert.Equal(cipher.DeletedDate, cipher.RevisionDate);
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipher);
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipher, null);
}
[Theory]
[BitAutoData]
public async Task SoftDeleteAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException(
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
cipher.UserId = Guid.NewGuid();
cipher.OrganizationId = null;
sutProvider.GetDependency<ICipherRepository>()
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
.Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId));
Assert.Contains("do not have permissions", exception.Message);
}
[Theory]
[OrganizationCipherCustomize]
[BitAutoData]
public async Task SoftDeleteAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException(
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
sutProvider.GetDependency<ICipherRepository>()
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
.Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId));
Assert.Contains("do not have permissions", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SoftDeleteAsync_WithCipherDetailsType_SoftDeletesCipherDetails(
Guid deletingUserId, CipherDetails cipher, SutProvider<CipherService> sutProvider)
{
cipher.DeletedDate = null;
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId, true);
Assert.NotNull(cipher.DeletedDate);
Assert.Equal(cipher.DeletedDate, cipher.RevisionDate);
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpsertAsync(cipher);
await sutProvider.GetDependency<IEventService>().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted);
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncCipherUpdateAsync(cipher, null);
}
[Theory]
[BitAutoData]
public async Task SoftDeleteAsync_WithAlreadySoftDeletedCipher_SkipsOperation(
Guid deletingUserId, Cipher cipher, SutProvider<CipherService> sutProvider)
{
sutProvider.GetDependency<ICipherRepository>()
.GetCanEditByIdAsync(deletingUserId, cipher.Id)
.Returns(true);
cipher.DeletedDate = DateTime.UtcNow.AddDays(-1);
await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpsertAsync(Arg.Any<Cipher>());
await sutProvider.GetDependency<IEventService>().DidNotReceive().LogCipherEventAsync(Arg.Any<Cipher>(), Arg.Any<EventType>());
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceive().PushSyncCipherUpdateAsync(Arg.Any<Cipher>(), Arg.Any<IEnumerable<Guid>>());
}
private async Task AssertNoActionsAsync(SutProvider<CipherService> sutProvider)
{
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default);