1
0
mirror of https://github.com/bitwarden/server synced 2026-02-26 09:23:28 +00:00

Merge branch 'main' into auth/pm-30810/http-redirect-cloud

This commit is contained in:
Patrick-Pimentel-Bitwarden
2026-02-13 17:08:04 -05:00
committed by GitHub
74 changed files with 4929 additions and 2745 deletions

View File

@@ -5,6 +5,7 @@ using Bit.Admin.Services;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Providers.Services;
@@ -12,7 +13,11 @@ using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using NSubstitute;
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
namespace Admin.Test.AdminConsole.Controllers;
@@ -299,18 +304,164 @@ public class OrganizationsControllerTests
.Returns(true);
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id);
sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())
.Returns(Valid(request));
// Act
_ = await sutProvider.Sut.Edit(organization.Id, update);
// Assert
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(o => o.Id == organization.Id
&& o.UseAutomaticUserConfirmation == true));
}
// Annul
await organizationRepository.DeleteAsync(organization);
[BitAutoData]
[SutProviderCustomize]
[Theory]
public async Task Edit_EnableUseAutomaticUserConfirmation_ValidationFails_RedirectsWithError(
Organization organization,
SutProvider<OrganizationsController> sutProvider)
{
// Arrange
var update = new OrganizationEditModel
{
PlanType = PlanType.TeamsMonthly,
UseAutomaticUserConfirmation = true
};
organization.UseAutomaticUserConfirmation = false;
sutProvider.GetDependency<IAccessControlService>()
.UserHasPermission(Permission.Org_Plan_Edit)
.Returns(true);
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id);
sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())
.Returns(Invalid(request, new UserNotCompliantWithSingleOrganization()));
sutProvider.Sut.TempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());
// Act
var result = await sutProvider.Sut.Edit(organization.Id, update);
// Assert
var redirectResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Edit", redirectResult.ActionName);
Assert.Equal(organization.Id, redirectResult.RouteValues!["id"]);
await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any<Organization>());
}
[BitAutoData]
[SutProviderCustomize]
[Theory]
public async Task Edit_EnableUseAutomaticUserConfirmation_ProviderValidationFails_RedirectsWithError(
Organization organization,
SutProvider<OrganizationsController> sutProvider)
{
// Arrange
var update = new OrganizationEditModel
{
PlanType = PlanType.TeamsMonthly,
UseAutomaticUserConfirmation = true
};
organization.UseAutomaticUserConfirmation = false;
sutProvider.GetDependency<IAccessControlService>()
.UserHasPermission(Permission.Org_Plan_Edit)
.Returns(true);
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id);
sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())
.Returns(Invalid(request, new ProviderExistsInOrganization()));
sutProvider.Sut.TempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());
// Act
var result = await sutProvider.Sut.Edit(organization.Id, update);
// Assert
var redirectResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Edit", redirectResult.ActionName);
await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any<Organization>());
}
[BitAutoData]
[SutProviderCustomize]
[Theory]
public async Task Edit_UseAutomaticUserConfirmation_NotChanged_DoesNotCallValidator(
SutProvider<OrganizationsController> sutProvider)
{
// Arrange
var organizationId = new Guid();
var update = new OrganizationEditModel
{
UseSecretsManager = false,
UseAutomaticUserConfirmation = false
};
var organization = new Organization
{
Id = organizationId,
UseAutomaticUserConfirmation = false
};
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(organization);
// Act
_ = await sutProvider.Sut.Edit(organizationId, update);
// Assert
await sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.DidNotReceive()
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>());
}
[BitAutoData]
[SutProviderCustomize]
[Theory]
public async Task Edit_UseAutomaticUserConfirmation_AlreadyEnabled_DoesNotCallValidator(
SutProvider<OrganizationsController> sutProvider)
{
// Arrange
var organizationId = new Guid();
var update = new OrganizationEditModel
{
UseSecretsManager = false,
UseAutomaticUserConfirmation = true
};
var organization = new Organization
{
Id = organizationId,
UseAutomaticUserConfirmation = true
};
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(organization);
// Act
_ = await sutProvider.Sut.Edit(organizationId, update);
// Assert
await sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.DidNotReceive()
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>());
}
#endregion

View File

@@ -162,7 +162,7 @@ public class OrganizationUserControllerAutoConfirmTests : IClassFixture<ApiAppli
var testKey = $"test-key-{Guid.NewGuid()}";
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com";
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(userToConfirmEmail);
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
@@ -190,15 +190,17 @@ public class OrganizationUserControllerAutoConfirmTests : IClassFixture<ApiAppli
new Permissions(),
OrganizationUserStatusType.Accepted);
var tenRequests = Enumerable.Range(0, 10)
.Select(_ => _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
var results = new List<HttpResponseMessage>();
foreach (var _ in Enumerable.Range(0, 10))
{
results.Add(await _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
new OrganizationUserConfirmRequestModel
{
Key = testKey,
DefaultUserCollectionName = _mockEncryptedString
})).ToList();
var results = await Task.WhenAll(tenRequests);
}));
}
Assert.Contains(results, r => r.StatusCode == HttpStatusCode.NoContent);

View File

@@ -1,13 +1,13 @@
#nullable enable
using Bit.Api.Controllers;
using Bit.Api.Platform.SsoCookieVendor;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Controllers;
namespace Bit.Api.Test.Platform.SsoCookieVendor.Controllers;
public class SsoCookieVendorControllerTests : IDisposable
{
@@ -129,7 +129,7 @@ public class SsoCookieVendorControllerTests : IDisposable
// Assert
var redirectResult = Assert.IsType<RedirectResult>(result);
Assert.Equal("bitwarden://sso_cookie_vendor?test-cookie=my-token-value-123&d=1", redirectResult.Url);
Assert.Equal("bitwarden://sso-cookie-vendor?test-cookie=my-token-value-123&d=1", redirectResult.Url);
}
[Fact]
@@ -170,7 +170,7 @@ public class SsoCookieVendorControllerTests : IDisposable
// Assert
var redirectResult = Assert.IsType<RedirectResult>(result);
Assert.StartsWith("bitwarden://sso_cookie_vendor?", redirectResult.Url);
Assert.StartsWith("bitwarden://sso-cookie-vendor?", redirectResult.Url);
Assert.Contains("test-cookie-0=part1", redirectResult.Url);
Assert.Contains("test-cookie-1=part2", redirectResult.Url);
Assert.Contains("test-cookie-2=part3", redirectResult.Url);
@@ -256,7 +256,7 @@ public class SsoCookieVendorControllerTests : IDisposable
public void Get_WhenUriExceedsMaxLength_Returns400()
{
// Arrange - create a very long cookie value that will exceed 8192 characters
// URI format: "bitwarden://sso_cookie_vendor?test-cookie={value}"
// URI format: "bitwarden://sso-cookie-vendor?test-cookie={value}"
// Base URI length is about 43 characters, so we need value > 8149
var longValue = new string('a', 8200);
var cookies = new Dictionary<string, string>
@@ -289,7 +289,7 @@ public class SsoCookieVendorControllerTests : IDisposable
// Assert
var redirectResult = Assert.IsType<RedirectResult>(result);
Assert.Equal("bitwarden://sso_cookie_vendor?test-cookie=single-value&d=1", redirectResult.Url);
Assert.Equal("bitwarden://sso-cookie-vendor?test-cookie=single-value&d=1", redirectResult.Url);
}
[Fact]

View File

@@ -6,6 +6,7 @@ using Bit.Api.Tools.Controllers;
using Bit.Api.Tools.Models;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Tools.Models.Response;
using Bit.Core;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
@@ -80,6 +81,8 @@ public class SendsControllerTests : IDisposable
send.Id = default;
send.Type = SendType.Text;
send.Data = JsonSerializer.Serialize(new Dictionary<string, string>());
send.AuthType = AuthType.None;
send.Emails = null;
send.HideEmail = true;
_sendRepository.GetByIdAsync(Arg.Any<Guid>()).Returns(send);
@@ -657,7 +660,7 @@ public class SendsControllerTests : IDisposable
}
[Theory, AutoData]
public async Task Put_WithoutPasswordOrEmails_PreservesExistingPassword(Guid userId, Guid sendId)
public async Task Put_WithoutPasswordOrEmails_ClearsExistingPassword(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
@@ -685,13 +688,13 @@ public class SendsControllerTests : IDisposable
Assert.Equal(sendId, result.Id);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.Id == sendId &&
s.AuthType == AuthType.Password &&
s.Password == "hashed-password" &&
s.AuthType == AuthType.None &&
s.Password == null &&
s.Emails == null));
}
[Theory, AutoData]
public async Task Put_WithoutPasswordOrEmails_PreservesExistingEmails(Guid userId, Guid sendId)
public async Task Put_WithoutPasswordOrEmails_ClearsExistingEmails(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
@@ -719,9 +722,9 @@ public class SendsControllerTests : IDisposable
Assert.Equal(sendId, result.Id);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.Id == sendId &&
s.AuthType == AuthType.Email &&
s.Emails == "test@example.com" &&
s.Password == null));
s.AuthType == AuthType.None &&
s.Password == null &&
s.Emails == null));
}
[Theory, AutoData]
@@ -793,6 +796,33 @@ public class SendsControllerTests : IDisposable
await _userService.Received(1).GetUserByIdAsync(creator.Id);
}
[Theory, AutoData]
public async Task AccessUsingAuth_WithEmailProtectedSend_WithFfDisabled_ReturnsUnauthorizedResult(Guid sendId, User creator)
{
var send = new Send
{
Id = sendId,
UserId = creator.Id,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
HideEmail = false,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
Disabled = false,
AccessCount = 0,
AuthType = AuthType.Email,
Emails = "test@example.com",
MaxAccessCount = null
};
var user = CreateUserWithSendIdClaim(sendId);
_sut.ControllerContext = CreateControllerContextWithUser(user);
_sendRepository.GetByIdAsync(sendId).Returns(send);
_userService.GetUserByIdAsync(creator.Id).Returns(creator);
_featureService.IsEnabled(FeatureFlagKeys.SendEmailOTP).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());
}
[Theory, AutoData]
public async Task AccessUsingAuth_WithHideEmail_DoesNotIncludeCreatorIdentifier(Guid sendId, User creator)
{
@@ -1036,6 +1066,33 @@ public class SendsControllerTests : IDisposable
await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId);
}
[Theory, AutoData]
public async Task GetSendFileDownloadDataUsingAuth_WithEmailProtectedSend_WithFfDisabled_ReturnsUnauthorizedResult(
Guid sendId, string fileId, string expectedUrl)
{
var fileData = new SendFileData("Test File", "Notes", "document.pdf") { Id = fileId, Size = 2048 };
var send = new Send
{
Id = sendId,
Type = SendType.File,
Data = JsonSerializer.Serialize(fileData),
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
Disabled = false,
AccessCount = 0,
AuthType = AuthType.Email,
Emails = "test@example.com",
MaxAccessCount = null
};
var user = CreateUserWithSendIdClaim(sendId);
_sut.ControllerContext = CreateControllerContextWithUser(user);
_sendRepository.GetByIdAsync(sendId).Returns(send);
_sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl);
_featureService.IsEnabled(FeatureFlagKeys.SendEmailOTP).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() => _sut.GetSendFileDownloadDataUsingAuth(fileId));
}
[Theory, AutoData]
public async Task GetSendFileDownloadDataUsingAuth_WithNonExistentSend_ThrowsBadRequestException(
Guid sendId, string fileId)

View File

@@ -315,14 +315,14 @@ public class SubscriptionUpdatedHandlerTests
}
[Fact]
public async Task HandleAsync_ProviderSubscription_WithIncompleteExpiredStatus_DoesNotDisableProvider()
public async Task HandleAsync_IncompleteToIncompleteExpiredTransition_DisablesProviderAndSetsCancellation()
{
// Arrange
var providerId = Guid.NewGuid();
var subscriptionId = "sub_123";
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
// Previous status that doesn't trigger enable/disable logic
// Previous status was Incomplete - this is the valid transition for IncompleteExpired
var previousSubscription = new Subscription
{
Id = subscriptionId,
@@ -341,7 +341,7 @@ public class SubscriptionUpdatedHandlerTests
]
},
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "renewal" }
LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCreate }
};
var provider = new Provider { Id = providerId, Name = "Test Provider", Enabled = true };
@@ -364,10 +364,142 @@ public class SubscriptionUpdatedHandlerTests
// Act
await _sut.HandleAsync(parsedEvent);
// Assert - IncompleteExpired status is not handled by the new logic
Assert.True(provider.Enabled);
await _providerService.DidNotReceive().UpdateAsync(Arg.Any<Provider>());
await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
// Assert - Incomplete to IncompleteExpired should trigger disable and cancellation
Assert.False(provider.Enabled);
await _providerService.Received(1).UpdateAsync(provider);
await _stripeFacade.Received(1).UpdateSubscription(
subscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options =>
options.CancelAt.HasValue &&
options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&
options.ProrationBehavior == ProrationBehavior.None &&
options.CancellationDetails != null &&
options.CancellationDetails.Comment != null));
}
[Fact]
public async Task HandleAsync_IncompleteToIncompleteExpiredUserSubscription_DisablesPremiumAndSetsCancellation()
{
// Arrange
var userId = Guid.NewGuid();
var subscriptionId = "sub_123";
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
var previousSubscription = new Subscription
{
Id = subscriptionId,
Status = SubscriptionStatus.Incomplete
};
var subscription = new Subscription
{
Id = subscriptionId,
Status = SubscriptionStatus.IncompleteExpired,
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } },
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
},
LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCreate }
};
var parsedEvent = new Event
{
Data = new EventData
{
Object = subscription,
PreviousAttributes = JObject.FromObject(previousSubscription)
}
};
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _userService.Received(1).DisablePremiumAsync(userId, currentPeriodEnd);
await _stripeFacade.Received(1).UpdateSubscription(
subscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options =>
options.CancelAt.HasValue &&
options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&
options.ProrationBehavior == ProrationBehavior.None &&
options.CancellationDetails != null &&
options.CancellationDetails.Comment != null));
}
[Fact]
public async Task HandleAsync_IncompleteToIncompleteExpiredOrganizationSubscription_DisablesOrganizationAndSetsCancellation()
{
// Arrange
var organizationId = Guid.NewGuid();
var subscriptionId = "sub_123";
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
var previousSubscription = new Subscription
{
Id = subscriptionId,
Status = SubscriptionStatus.Incomplete
};
var subscription = new Subscription
{
Id = subscriptionId,
Status = SubscriptionStatus.IncompleteExpired,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = currentPeriodEnd,
Plan = new Plan { Id = "2023-enterprise-org-seat-annually" }
}
]
},
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } },
LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCreate }
};
var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 };
var parsedEvent = new Event
{
Data = new EventData
{
Object = subscription,
PreviousAttributes = JObject.FromObject(previousSubscription)
}
};
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
var plan = new Enterprise2023Plan(true);
_pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);
_pricingClient.ListPlans().Returns(MockPlans.Plans);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _organizationDisableCommand.Received(1).DisableAsync(organizationId, currentPeriodEnd);
await _pushNotificationAdapter.Received(1).NotifyEnabledChangedAsync(organization);
await _stripeFacade.Received(1).UpdateSubscription(
subscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options =>
options.CancelAt.HasValue &&
options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&
options.ProrationBehavior == ProrationBehavior.None &&
options.CancellationDetails != null &&
options.CancellationDetails.Comment != null));
}
[Fact]
@@ -470,6 +602,9 @@ public class SubscriptionUpdatedHandlerTests
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
// Act
await _sut.HandleAsync(parsedEvent);
@@ -484,6 +619,10 @@ public class SubscriptionUpdatedHandlerTests
options.ProrationBehavior == ProrationBehavior.None &&
options.CancellationDetails != null &&
options.CancellationDetails.Comment != null));
await _stripeFacade.DidNotReceive()
.CancelSubscription(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
await _stripeFacade.DidNotReceive()
.ListInvoices(Arg.Any<InvoiceListOptions>());
}
[Fact]
@@ -527,6 +666,9 @@ public class SubscriptionUpdatedHandlerTests
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
// Act
await _sut.HandleAsync(parsedEvent);
@@ -534,6 +676,10 @@ public class SubscriptionUpdatedHandlerTests
await _userService.DidNotReceive().DisablePremiumAsync(Arg.Any<Guid>(), Arg.Any<DateTime?>());
await _userService.Received(1).UpdatePremiumExpirationAsync(userId, currentPeriodEnd);
await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
await _stripeFacade.DidNotReceive()
.CancelSubscription(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
await _stripeFacade.DidNotReceive()
.ListInvoices(Arg.Any<InvoiceListOptions>());
}
[Fact]

View File

@@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
@@ -767,6 +768,50 @@ public class AcceptOrgUserCommandTests
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUser_WithAutoConfirmFeatureFlagEnabled_SendsPushNotification(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
.Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user)));
await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
await sutProvider.GetDependency<IPushAutoConfirmNotificationCommand>()
.Received(1)
.PushAsync(user.Id, orgUser.OrganizationId);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUser_WithAutoConfirmFeatureFlagDisabled_DoesNotSendPushNotification(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(false);
await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
await sutProvider.GetDependency<IPushAutoConfirmNotificationCommand>()
.DidNotReceiveWithAnyArgs()
.PushAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
private void SetupCommonAcceptOrgUserByTokenMocks(SutProvider<AcceptOrgUserCommand> sutProvider, User user, OrganizationUser orgUser)
{
sutProvider.GetDependency<IGlobalSettings>().OrganizationInviteExpirationHours.Returns(24);

View File

@@ -0,0 +1,286 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
[SutProviderCustomize]
public class PushAutoConfirmNotificationCommandTests
{
[Theory]
[BitAutoData]
public async Task PushAsync_SendsNotificationToAdminsAndOwners(
SutProvider<PushAutoConfirmNotificationCommand> sutProvider,
Guid userId,
Guid organizationId,
OrganizationUser orgUser,
List<OrganizationUserUserDetails> admins)
{
foreach (var admin in admins)
{
admin.UserId = Guid.NewGuid();
}
orgUser.Id = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns(orgUser);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)
.Returns(admins);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)
.Returns(new List<OrganizationUserUserDetails>());
await sutProvider.Sut.PushAsync(userId, organizationId);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(admins.Count)
.PushAsync(Arg.Is<PushNotification<AutoConfirmPushNotification>>(pn =>
pn.Type == PushType.AutoConfirm &&
pn.Target == NotificationTarget.User &&
pn.Payload.OrganizationId == organizationId &&
pn.Payload.TargetUserId == orgUser.Id &&
pn.ExcludeCurrentContext == false));
}
[Theory]
[BitAutoData]
public async Task PushAsync_SendsNotificationToCustomUsersWithManageUsersPermission(
SutProvider<PushAutoConfirmNotificationCommand> sutProvider,
Guid userId,
Guid organizationId,
OrganizationUser orgUser,
List<OrganizationUserUserDetails> customUsers)
{
foreach (var customUser in customUsers)
{
customUser.UserId = Guid.NewGuid();
customUser.Permissions = "{\"manageUsers\":true}";
}
orgUser.Id = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns(orgUser);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)
.Returns(new List<OrganizationUserUserDetails>());
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)
.Returns(customUsers);
await sutProvider.Sut.PushAsync(userId, organizationId);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(customUsers.Count)
.PushAsync(Arg.Is<PushNotification<AutoConfirmPushNotification>>(pn =>
pn.Type == PushType.AutoConfirm &&
pn.Target == NotificationTarget.User &&
pn.Payload.OrganizationId == organizationId &&
pn.Payload.TargetUserId == orgUser.Id &&
pn.ExcludeCurrentContext == false));
}
[Theory]
[BitAutoData]
public async Task PushAsync_DoesNotSendToCustomUsersWithoutManageUsersPermission(
SutProvider<PushAutoConfirmNotificationCommand> sutProvider,
Guid userId,
Guid organizationId,
OrganizationUser orgUser,
List<OrganizationUserUserDetails> customUsers)
{
foreach (var customUser in customUsers)
{
customUser.UserId = Guid.NewGuid();
customUser.Permissions = "{\"manageUsers\":false}";
}
orgUser.Id = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns(orgUser);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)
.Returns(new List<OrganizationUserUserDetails>());
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)
.Returns(customUsers);
await sutProvider.Sut.PushAsync(userId, organizationId);
await sutProvider.GetDependency<IPushNotificationService>()
.DidNotReceiveWithAnyArgs()
.PushAsync(Arg.Any<PushNotification<AutoConfirmPushNotification>>());
}
[Theory]
[BitAutoData]
public async Task PushAsync_SendsToAdminsAndCustomUsersWithManageUsers(
SutProvider<PushAutoConfirmNotificationCommand> sutProvider,
Guid userId,
Guid organizationId,
OrganizationUser orgUser,
List<OrganizationUserUserDetails> admins,
List<OrganizationUserUserDetails> customUsersWithPermission,
List<OrganizationUserUserDetails> customUsersWithoutPermission)
{
foreach (var admin in admins)
{
admin.UserId = Guid.NewGuid();
}
foreach (var customUser in customUsersWithPermission)
{
customUser.UserId = Guid.NewGuid();
customUser.Permissions = "{\"manageUsers\":true}";
}
foreach (var customUser in customUsersWithoutPermission)
{
customUser.UserId = Guid.NewGuid();
customUser.Permissions = "{\"manageUsers\":false}";
}
orgUser.Id = Guid.NewGuid();
var allCustomUsers = customUsersWithPermission.Concat(customUsersWithoutPermission).ToList();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns(orgUser);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)
.Returns(admins);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)
.Returns(allCustomUsers);
await sutProvider.Sut.PushAsync(userId, organizationId);
var expectedNotificationCount = admins.Count + customUsersWithPermission.Count;
await sutProvider.GetDependency<IPushNotificationService>()
.Received(expectedNotificationCount)
.PushAsync(Arg.Is<PushNotification<AutoConfirmPushNotification>>(pn =>
pn.Type == PushType.AutoConfirm &&
pn.Target == NotificationTarget.User &&
pn.Payload.OrganizationId == organizationId &&
pn.Payload.TargetUserId == orgUser.Id &&
pn.ExcludeCurrentContext == false));
}
[Theory]
[BitAutoData]
public async Task PushAsync_SkipsUsersWithoutUserId(
SutProvider<PushAutoConfirmNotificationCommand> sutProvider,
Guid userId,
Guid organizationId,
OrganizationUser orgUser,
List<OrganizationUserUserDetails> admins)
{
admins[0].UserId = Guid.NewGuid();
admins[1].UserId = null;
admins[2].UserId = Guid.NewGuid();
orgUser.Id = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns(orgUser);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)
.Returns(admins);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)
.Returns(new List<OrganizationUserUserDetails>());
await sutProvider.Sut.PushAsync(userId, organizationId);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(2)
.PushAsync(Arg.Is<PushNotification<AutoConfirmPushNotification>>(pn =>
pn.Type == PushType.AutoConfirm));
}
[Theory]
[BitAutoData]
public async Task PushAsync_DeduplicatesUserIds(
SutProvider<PushAutoConfirmNotificationCommand> sutProvider,
Guid userId,
Guid organizationId,
OrganizationUser orgUser,
Guid duplicateUserId)
{
var admin1 = new OrganizationUserUserDetails { UserId = duplicateUserId };
var admin2 = new OrganizationUserUserDetails { UserId = duplicateUserId };
var customUser = new OrganizationUserUserDetails
{
UserId = duplicateUserId,
Permissions = "{\"manageUsers\":true}"
};
orgUser.Id = Guid.NewGuid();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns(orgUser);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)
.Returns(new List<OrganizationUserUserDetails> { admin1, admin2 });
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)
.Returns(new List<OrganizationUserUserDetails> { customUser });
await sutProvider.Sut.PushAsync(userId, organizationId);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushAsync(Arg.Is<PushNotification<AutoConfirmPushNotification>>(pn =>
pn.TargetId == duplicateUserId));
}
[Theory]
[BitAutoData]
public async Task PushAsync_OrganizationUserNotFound_ThrowsException(
SutProvider<PushAutoConfirmNotificationCommand> sutProvider,
Guid userId,
Guid organizationId)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationId, userId)
.Returns((OrganizationUser)null);
var exception = await Assert.ThrowsAsync<Exception>(() =>
sutProvider.Sut.PushAsync(userId, organizationId));
Assert.Equal("Organization user not found", exception.Message);
await sutProvider.GetDependency<IPushNotificationService>()
.DidNotReceiveWithAnyArgs()
.PushAsync(Arg.Any<PushNotification<AutoConfirmPushNotification>>());
}
}

View File

@@ -0,0 +1,544 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
[SutProviderCustomize]
public class AutomaticUserConfirmationOrganizationPolicyComplianceValidatorTests
{
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_AllUsersCompliant_NoProviders_ReturnsValid(
Guid organizationId,
Guid userId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange
var orgUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = userId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([orgUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsValid);
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_UserInAnotherOrg_ReturnsUserNotCompliantWithSingleOrganization(
Guid organizationId,
Guid userId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange
var orgUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = userId,
Status = OrganizationUserStatusType.Confirmed
};
var otherOrgUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(), // Different org
UserId = userId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([orgUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([otherOrgUser]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsError);
Assert.IsType<UserNotCompliantWithSingleOrganization>(result.AsError);
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_ProviderUsersExist_ReturnsProviderExistsInOrganization(
Guid organizationId,
Guid userId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange
var orgUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = userId,
Status = OrganizationUserStatusType.Confirmed
};
var providerUser = new ProviderUser
{
Id = Guid.NewGuid(),
ProviderId = Guid.NewGuid(),
UserId = userId
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([orgUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([providerUser]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsError);
Assert.IsType<ProviderExistsInOrganization>(result.AsError);
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_InvitedUsersExcluded_FromSingleOrgCheck(
Guid organizationId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange - invited user has null UserId and Invited status
var invitedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = null,
Status = OrganizationUserStatusType.Invited,
Email = "invited@example.com"
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([invitedUser]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsValid);
// Invited users with null UserId should not trigger the single org query
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()));
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_InvitedUserWithUserId_ExcludedFromSingleOrgCheck(
Guid organizationId,
Guid userId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange - Invited status users are excluded regardless of UserId
var invitedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = userId,
Status = OrganizationUserStatusType.Invited
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([invitedUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsValid);
// Invited users should not be included in the single org compliance query
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()));
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_UserInAnotherOrgWithInvitedStatus_ReturnsValid(
Guid organizationId,
Guid userId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange
var orgUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = userId,
Status = OrganizationUserStatusType.Confirmed
};
// User has an Invited status in another org - should not count as non-compliant
var otherOrgUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
UserId = userId,
Status = OrganizationUserStatusType.Invited
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([orgUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([otherOrgUser]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsValid);
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_SingleOrgViolationTakesPrecedence_OverProviderCheck(
Guid organizationId,
Guid userId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange - user is in another org AND is a provider user
var orgUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = userId,
Status = OrganizationUserStatusType.Confirmed
};
var otherOrgUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
UserId = userId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([orgUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([otherOrgUser]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsError);
Assert.IsType<UserNotCompliantWithSingleOrganization>(result.AsError);
// Provider check should not be called since single org check failed first
await sutProvider.GetDependency<IProviderUserRepository>()
.DidNotReceive()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>());
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_MixedUsers_OnlyNonInvitedChecked(
Guid organizationId,
Guid confirmedUserId,
Guid acceptedUserId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange
var invitedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = null,
Status = OrganizationUserStatusType.Invited,
Email = "invited@example.com"
};
var confirmedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = confirmedUserId,
Status = OrganizationUserStatusType.Confirmed
};
var acceptedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = acceptedUserId,
Status = OrganizationUserStatusType.Accepted
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([invitedUser, confirmedUser, acceptedUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsValid);
// Only confirmed and accepted users should be checked for single org compliance
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids =>
ids.Count() == 2 &&
ids.Contains(confirmedUserId) &&
ids.Contains(acceptedUserId)));
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_NoOrganizationUsers_ReturnsValid(
Guid organizationId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsValid);
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_UserInSameOrgOnly_ReturnsValid(
Guid organizationId,
Guid userId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange
var orgUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = userId,
Status = OrganizationUserStatusType.Confirmed
};
// User exists in the same org only (the GetManyByManyUsersAsync returns same-org entry)
var sameOrgUser = new OrganizationUser
{
Id = orgUser.Id,
OrganizationId = organizationId,
UserId = userId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([orgUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([sameOrgUser]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsValid);
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_ProviderCheckIncludesAllUsersWithUserIds(
Guid organizationId,
Guid userId1,
Guid userId2,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange - provider check includes users regardless of Invited status (only excludes null UserId)
var confirmedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = userId1,
Status = OrganizationUserStatusType.Confirmed
};
var invitedUserWithNullId = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = null,
Status = OrganizationUserStatusType.Invited,
Email = "invited@example.com"
};
var acceptedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = userId2,
Status = OrganizationUserStatusType.Accepted
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([confirmedUser, invitedUserWithNullId, acceptedUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsValid);
// Provider check should include all users with non-null UserIds (confirmed + accepted)
await sutProvider.GetDependency<IProviderUserRepository>()
.Received(1)
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids =>
ids.Count() == 2 &&
ids.Contains(userId1) &&
ids.Contains(userId2)));
}
[Theory, BitAutoData]
public async Task IsOrganizationCompliantAsync_RevokedUserInAnotherOrg_ReturnsUserNotCompliant(
Guid organizationId,
Guid userId,
SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)
{
// Arrange
var revokedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
UserId = userId,
Status = OrganizationUserStatusType.Revoked
};
var otherOrgUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
UserId = userId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns([revokedUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([otherOrgUser]);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);
// Act
var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);
// Assert
Assert.True(result.IsError);
Assert.IsType<UserNotCompliantWithSingleOrganization>(result.AsError);
}
}

View File

@@ -1,19 +1,14 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
@@ -34,35 +29,14 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_UsersNotCompliantWithSingleOrg_ReturnsError(
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
Guid nonCompliantUserId,
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
{
// Arrange
var orgUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = policyUpdate.OrganizationId,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = nonCompliantUserId,
Email = "user@example.com"
};
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId);
var otherOrgUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
UserId = nonCompliantUserId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([orgUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([otherOrgUser]);
sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())
.Returns(Invalid(request, new UserNotCompliantWithSingleOrganization()));
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
@@ -71,85 +45,17 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests
Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_UserWithInvitedStatusInOtherOrg_ValidationPasses(
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
Guid userId,
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
{
// Arrange
var orgUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = policyUpdate.OrganizationId,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = userId,
};
var otherOrgUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
UserId = null, // invited users do not have a user id
Status = OrganizationUserStatusType.Invited,
Email = orgUser.Email
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([orgUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([otherOrgUser]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.True(string.IsNullOrEmpty(result));
}
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_ProviderUsersExist_ReturnsError(
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
Guid userId,
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
{
// Arrange
var orgUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = policyUpdate.OrganizationId,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = userId
};
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId);
var providerUser = new ProviderUser
{
Id = Guid.NewGuid(),
ProviderId = Guid.NewGuid(),
UserId = userId,
Status = ProviderUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([orgUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([providerUser]);
sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())
.Returns(Invalid(request, new ProviderExistsInOrganization()));
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
@@ -158,33 +64,17 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests
Assert.Contains("Provider user type", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_AllValidationsPassed_ReturnsEmptyString(
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
{
// Arrange
var orgUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = policyUpdate.OrganizationId,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = Guid.NewGuid()
};
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([orgUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())
.Returns(Valid(request));
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
@@ -208,9 +98,9 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests
// Assert
Assert.True(string.IsNullOrEmpty(result));
await sutProvider.GetDependency<IOrganizationUserRepository>()
await sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.DidNotReceive()
.GetManyDetailsByOrganizationAsync(Arg.Any<Guid>());
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>());
}
[Theory, BitAutoData]
@@ -227,212 +117,31 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests
// Assert
Assert.True(string.IsNullOrEmpty(result));
await sutProvider.GetDependency<IOrganizationUserRepository>()
await sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.DidNotReceive()
.GetManyDetailsByOrganizationAsync(Arg.Any<Guid>());
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>());
}
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_IncludesOwnersAndAdmins_InComplianceCheck(
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
Guid nonCompliantOwnerId,
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
{
// Arrange
var ownerUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = policyUpdate.OrganizationId,
Type = OrganizationUserType.Owner,
Status = OrganizationUserStatusType.Confirmed,
UserId = nonCompliantOwnerId,
};
var otherOrgUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
UserId = nonCompliantOwnerId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([ownerUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([otherOrgUser]);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_InvitedUsersExcluded_FromComplianceCheck(
public async Task ValidateAsync_EnablingPolicy_PassesCorrectOrganizationId(
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
{
// Arrange
var invitedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = policyUpdate.OrganizationId,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Invited,
UserId = null,
Email = "invited@example.com"
};
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([invitedUser]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())
.Returns(Valid(request));
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.True(string.IsNullOrEmpty(result));
}
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_MixedUsersWithNullUserId_HandlesCorrectly(
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
Guid confirmedUserId,
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
{
// Arrange
var invitedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = policyUpdate.OrganizationId,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Invited,
UserId = null,
Email = "invited@example.com"
};
var confirmedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = policyUpdate.OrganizationId,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = confirmedUserId,
Email = "confirmed@example.com"
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([invitedUser, confirmedUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.True(string.IsNullOrEmpty(result));
await sutProvider.GetDependency<IOrganizationUserRepository>()
await sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.Received(1)
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 1 && ids.First() == confirmedUserId));
}
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_RevokedUsersIncluded_InComplianceCheck(
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
{
// Arrange
var revokedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = policyUpdate.OrganizationId,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Revoked,
UserId = Guid.NewGuid(),
};
var additionalOrgUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Revoked,
UserId = revokedUser.UserId,
};
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
orgUserRepository
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([revokedUser]);
orgUserRepository.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([additionalOrgUser]);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([]);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase);
}
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_AcceptedUsersIncluded_InComplianceCheck(
[PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,
Guid nonCompliantUserId,
SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)
{
// Arrange
var acceptedUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = policyUpdate.OrganizationId,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Accepted,
UserId = nonCompliantUserId,
};
var otherOrgUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
UserId = nonCompliantUserId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([acceptedUser]);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
.Returns([otherOrgUser]);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.Contains("compliant with the Single organization policy", result, StringComparison.OrdinalIgnoreCase);
.IsOrganizationCompliantAsync(Arg.Is<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>(
r => r.OrganizationId == policyUpdate.OrganizationId));
}
[Theory, BitAutoData]
@@ -442,10 +151,11 @@ public class AutomaticUserConfirmationPolicyEventHandlerTests
{
// Arrange
var savePolicyModel = new SavePolicyModel(policyUpdate);
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([]);
sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())
.Returns(Valid(request));
// Act
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);

View File

@@ -4,7 +4,6 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.Services;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -20,10 +19,6 @@ public class BlockClaimedDomainAccountCreationPolicyValidatorTests
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
.Returns(false);
@@ -41,10 +36,6 @@ public class BlockClaimedDomainAccountCreationPolicyValidatorTests
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
.Returns(true);
@@ -61,11 +52,6 @@ public class BlockClaimedDomainAccountCreationPolicyValidatorTests
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, false)] PolicyUpdate policyUpdate,
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
@@ -82,10 +68,6 @@ public class BlockClaimedDomainAccountCreationPolicyValidatorTests
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
.Returns(false);
@@ -105,10 +87,6 @@ public class BlockClaimedDomainAccountCreationPolicyValidatorTests
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
.Returns(true);
@@ -128,10 +106,6 @@ public class BlockClaimedDomainAccountCreationPolicyValidatorTests
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
// Act
@@ -144,31 +118,11 @@ public class BlockClaimedDomainAccountCreationPolicyValidatorTests
.HasVerifiedDomainsAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task ValidateAsync_FeatureFlagDisabled_ReturnsError(
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(false);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.Equal("This feature is not enabled", result);
await sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.DidNotReceive()
.HasVerifiedDomainsAsync(Arg.Any<Guid>());
}
[Fact]
public void Type_ReturnsBlockClaimedDomainAccountCreation()
{
// Arrange
var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null, null);
var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null);
// Act & Assert
Assert.Equal(PolicyType.BlockClaimedDomainAccountCreation, validator.Type);
@@ -178,7 +132,7 @@ public class BlockClaimedDomainAccountCreationPolicyValidatorTests
public void RequiredPolicies_ReturnsEmpty()
{
// Arrange
var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null, null);
var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null);
// Act
var requiredPolicies = validator.RequiredPolicies.ToList();

View File

@@ -106,9 +106,14 @@ public class RegisterUserCommandTests
{
// Arrange
user.Id = Guid.NewGuid();
user.Email = $"test+{Guid.NewGuid()}@example.com";
organization.Id = Guid.NewGuid();
organization.Name = "Test Organization";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), organization.Id)
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user)
.Returns(IdentityResult.Success);
@@ -134,6 +139,12 @@ public class RegisterUserCommandTests
SutProvider<RegisterUserCommand> sutProvider)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), organization.Id)
.Returns(false);
var expectedError = new IdentityError();
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user)
@@ -161,9 +172,14 @@ public class RegisterUserCommandTests
SutProvider<RegisterUserCommand> sutProvider)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
organization.PlanType = planType;
organization.Name = "Enterprise Org";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), organization.Id)
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user)
.Returns(IdentityResult.Success);
@@ -192,6 +208,12 @@ public class RegisterUserCommandTests
SutProvider<RegisterUserCommand> sutProvider)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), organization.Id)
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user)
.Returns(IdentityResult.Success);
@@ -220,8 +242,13 @@ public class RegisterUserCommandTests
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
user.ReferenceData = null;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user, masterPasswordHash)
.Returns(IdentityResult.Success);
@@ -247,6 +274,12 @@ public class RegisterUserCommandTests
[Policy(PolicyType.TwoFactorAuthentication, true)] PolicyStatus policy)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), Arg.Any<Guid?>())
.Returns(false);
sutProvider.GetDependency<IGlobalSettings>()
.DisableUserRegistration.Returns(false);
@@ -350,6 +383,12 @@ public class RegisterUserCommandTests
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid? orgUserId)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IGlobalSettings>()
.DisableUserRegistration.Returns(true);
@@ -388,6 +427,12 @@ public class RegisterUserCommandTests
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid? orgUserId)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IGlobalSettings>()
.DisableUserRegistration.Returns(false);
@@ -457,10 +502,6 @@ public class RegisterUserCommandTests
.GetByIdAsync(orgUserId)
.Returns(orgUser);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Mock the new overload that excludes the organization - it should return true (domain IS blocked by another org)
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com", orgUser.OrganizationId)
@@ -504,10 +545,6 @@ public class RegisterUserCommandTests
.GetByIdAsync(orgUserId)
.Returns(orgUser);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Mock the new overload - it should return false (domain is NOT blocked by OTHER orgs)
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("company-domain.com", orgUser.OrganizationId)
@@ -541,6 +578,10 @@ public class RegisterUserCommandTests
orgUser.Email = user.Email;
orgUser.Id = orgUserId;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), Arg.Any<Guid?>())
.Returns(false);
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
@@ -644,6 +685,12 @@ public class RegisterUserCommandTests
public async Task RegisterUserViaEmailVerificationToken_DisabledOpenRegistration_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IGlobalSettings>()
.DisableUserRegistration = true;
@@ -721,6 +768,12 @@ public class RegisterUserCommandTests
string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IGlobalSettings>()
.DisableUserRegistration = true;
@@ -811,6 +864,12 @@ public class RegisterUserCommandTests
string masterPasswordHash, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IGlobalSettings>()
.DisableUserRegistration = true;
@@ -931,6 +990,8 @@ public class RegisterUserCommandTests
User user, string masterPasswordHash, Guid providerUserId)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
// Start with plaintext
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
@@ -950,6 +1011,10 @@ public class RegisterUserCommandTests
.CreateProtector("ProviderServiceDataProtector")
.Returns(mockDataProtector);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IGlobalSettings>()
.DisableUserRegistration = true;
@@ -975,10 +1040,6 @@ public class RegisterUserCommandTests
// Arrange
user.Email = "user@blocked-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
.Returns(true);
@@ -1002,10 +1063,6 @@ public class RegisterUserCommandTests
// Arrange
user.Email = "user@allowed-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowed-domain.com")
.Returns(false);
@@ -1038,9 +1095,14 @@ public class RegisterUserCommandTests
SutProvider<RegisterUserCommand> sutProvider)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
organization.PlanType = planType;
organization.Name = "Family Org";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), organization.Id)
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user)
.Returns(IdentityResult.Success);
@@ -1071,10 +1133,6 @@ public class RegisterUserCommandTests
// Arrange
user.Email = "user@blocked-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
.Returns(true);
@@ -1102,10 +1160,6 @@ public class RegisterUserCommandTests
// Arrange
user.Email = "user@blocked-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
.Returns(true);
@@ -1131,10 +1185,6 @@ public class RegisterUserCommandTests
emergencyAccess.Email = user.Email;
emergencyAccess.Id = acceptEmergencyAccessId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
.Returns(true);
@@ -1183,10 +1233,6 @@ public class RegisterUserCommandTests
sutProvider.GetDependency<IGlobalSettings>()
.OrganizationInviteExpirationHours.Returns(120); // 5 days
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
.Returns(true);
@@ -1213,10 +1259,6 @@ public class RegisterUserCommandTests
// Arrange
user.Email = "invalid-email-format";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUser(user));
@@ -1232,10 +1274,6 @@ public class RegisterUserCommandTests
// Arrange
user.Email = "invalid-email-format";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
.Returns(callInfo =>
@@ -1261,9 +1299,14 @@ public class RegisterUserCommandTests
SutProvider<RegisterUserCommand> sutProvider)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
user.ReferenceData = null;
orgUser.Email = user.Email;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), Arg.Any<Guid?>())
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user, masterPasswordHash)
.Returns(IdentityResult.Success);
@@ -1310,11 +1353,16 @@ public class RegisterUserCommandTests
SutProvider<RegisterUserCommand> sutProvider)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
Organization organization = new Organization
{
Name = null
};
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), Arg.Any<Guid?>())
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user)
.Returns(IdentityResult.Success);
@@ -1348,10 +1396,15 @@ public class RegisterUserCommandTests
SutProvider<RegisterUserCommand> sutProvider)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
user.ReferenceData = null;
orgUser.Email = user.Email;
organization.PlanType = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), Arg.Any<Guid?>())
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user, masterPasswordHash)
.Returns(IdentityResult.Success);
@@ -1406,10 +1459,6 @@ public class RegisterUserCommandTests
// Arrange
user.Email = "user@blocked-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com", organization.Id)
.Returns(true);
@@ -1429,10 +1478,6 @@ public class RegisterUserCommandTests
// Arrange
user.Email = "user@company-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Domain is claimed by THIS organization, so it should be allowed
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("company-domain.com", organization.Id)
@@ -1461,10 +1506,6 @@ public class RegisterUserCommandTests
// Arrange
user.Email = "user@unclaimed-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("unclaimed-domain.com", organization.Id)
.Returns(false); // Domain is not claimed by any org

View File

@@ -59,9 +59,11 @@ public class SendVerificationEmailForRegistrationCommandTests
[Theory]
[BitAutoData]
public async Task SendVerificationEmailForRegistrationCommand_WhenFromMarketingIsPremium_SendsEmailWithMarketingParameterAndReturnsNull(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
string email, string name, bool receiveMarketingEmails)
string name, bool receiveMarketingEmails)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.ReturnsNull();
@@ -167,9 +169,15 @@ public class SendVerificationEmailForRegistrationCommandTests
[Theory]
[BitAutoData]
public async Task SendVerificationEmailForRegistrationCommand_WhenOpenRegistrationDisabled_ThrowsBadRequestException(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
string email, string name, bool receiveMarketingEmails)
string name, bool receiveMarketingEmails)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = true;
@@ -235,10 +243,6 @@ public class SendVerificationEmailForRegistrationCommandTests
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blockedcompany.com")
.Returns(true);
@@ -266,10 +270,6 @@ public class SendVerificationEmailForRegistrationCommandTests
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowedcompany.com")
.Returns(false);
@@ -298,10 +298,6 @@ public class SendVerificationEmailForRegistrationCommandTests
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));

View File

@@ -0,0 +1,157 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Fido2NetLib;
using Fido2NetLib.Objects;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;
[SutProviderCustomize]
public class CompleteTwoFactorWebAuthnRegistrationCommandTests
{
/// <summary>
/// The "Start" command will have set the in-process credential registration request to "pending" status.
/// The purpose of Complete is to consume and enshrine this pending credential.
/// </summary>
private static void SetupWebAuthnProviderWithPending(User user, int credentialCount)
{
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
var metadata = new Dictionary<string, object>();
// Add existing credentials
for (var i = 1; i <= credentialCount; i++)
{
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
{
Name = $"Key {i}",
Descriptor = new PublicKeyCredentialDescriptor([(byte)i]),
PublicKey = [(byte)i],
UserHandle = [(byte)i],
SignatureCounter = 0,
CredType = "public-key",
RegDate = DateTime.UtcNow,
AaGuid = Guid.NewGuid()
};
}
// Add pending registration
var pendingOptions = new CredentialCreateOptions
{
Challenge = [1, 2, 3],
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
User = new Fido2User
{
Id = user.Id.ToByteArray(),
Name = user.Email ?? "test@example.com",
DisplayName = user.Name ?? "Test User"
},
PubKeyCredParams = []
};
metadata["pending"] = pendingOptions.ToJson();
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider { Enabled = true, MetaData = metadata };
user.SetTwoFactorProviders(providers);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task CompleteWebAuthRegistrationAsync_BelowLimit_Succeeds(bool hasPremium,
SutProvider<CompleteTwoFactorWebAuthnRegistrationCommand> sutProvider, User user,
AuthenticatorAttestationRawResponse deviceResponse)
{
// Arrange - User should have 1 available credential, determined by maximum allowed for tested Premium status.
var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;
user.Premium = hasPremium;
user.Id = Guid.NewGuid();
user.Email = "test@example.com";
sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);
SetupWebAuthnProviderWithPending(user,
credentialCount: hasPremium
? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials - 1
: maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials - 1);
var mockFido2 = sutProvider.GetDependency<IFido2>();
mockFido2.MakeNewCredentialAsync(
Arg.Any<AuthenticatorAttestationRawResponse>(),
Arg.Any<CredentialCreateOptions>(),
Arg.Any<IsCredentialIdUniqueToUserAsyncDelegate>())
.Returns(new Fido2.CredentialMakeResult("ok", "",
new AttestationVerificationSuccess
{
Aaguid = Guid.NewGuid(),
Counter = 0,
CredentialId = [1, 2, 3],
CredType = "public-key",
PublicKey = [4, 5, 6],
Status = "ok",
User = new Fido2User
{
Id = user.Id.ToByteArray(),
Name = user.Email ?? "test@example.com",
DisplayName = user.Name ?? "Test User"
}
}));
// Act
var result =
await sutProvider.Sut.CompleteTwoFactorWebAuthnRegistrationAsync(user, 5, "NewKey", deviceResponse);
// Assert
// Note that, contrary to the "Start" command, "Complete" does not suppress logging for the update providers invocation.
Assert.True(result);
await sutProvider.GetDependency<IUserService>().Received(1)
.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task CompleteWebAuthRegistrationAsync_ExceedsLimit_ThrowsBadRequestException(bool hasPremium,
SutProvider<CompleteTwoFactorWebAuthnRegistrationCommand> sutProvider, User user,
AuthenticatorAttestationRawResponse deviceResponse)
{
// Arrange - time-of-check/time-of-use scenario: user now has 10 credentials (at limit)
var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;
user.Premium = hasPremium;
sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);
SetupWebAuthnProviderWithPending(user,
credentialCount: hasPremium
? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials
: maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CompleteTwoFactorWebAuthnRegistrationAsync(user, 11, "NewKey", deviceResponse));
Assert.Equal("Maximum allowed WebAuthn credential count exceeded.", exception.Message);
}
}

View File

@@ -0,0 +1,138 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
using Bit.Core.Entities;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Fido2NetLib.Objects;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;
[SutProviderCustomize]
public class DeleteTwoFactorWebAuthnCredentialCommandTests
{
private static void SetupWebAuthnProvider(User user, int credentialCount)
{
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
var metadata = new Dictionary<string, object>();
// Add credentials as Key1, Key2, Key3, etc.
for (var i = 1; i <= credentialCount; i++)
{
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
{
Name = $"Key {i}",
Descriptor = new PublicKeyCredentialDescriptor([(byte)i]),
PublicKey = [(byte)i],
UserHandle = [(byte)i],
SignatureCounter = 0,
CredType = "public-key",
RegDate = DateTime.UtcNow,
AaGuid = Guid.NewGuid()
};
}
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider { Enabled = true, MetaData = metadata };
user.SetTwoFactorProviders(providers);
}
/// <summary>
/// When the user has multiple WebAuthn credentials and requests deletion of an existing key,
/// the command should remove it, persist via UserService, and return true.
/// </summary>
[Theory, BitAutoData]
public async Task DeleteAsync_KeyExistsWithMultipleKeys_RemovesKeyAndReturnsTrue(
SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)
{
// Arrange
SetupWebAuthnProvider(user, 3);
var keyIdToDelete = 2;
// Act
var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, keyIdToDelete);
// Assert
Assert.True(result);
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
Assert.NotNull(provider?.MetaData);
Assert.False(provider.MetaData.ContainsKey($"Key{keyIdToDelete}"));
Assert.Equal(2, provider.MetaData.Count);
await sutProvider.GetDependency<IUserService>().Received(1)
.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);
}
/// <summary>
/// When the requested key does not exist, the command should return false
/// and not call UserService.
/// </summary>
[Theory, BitAutoData]
public async Task DeleteAsync_KeyDoesNotExist_ReturnsFalse(
SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)
{
// Arrange
SetupWebAuthnProvider(user, 2);
var nonExistentKeyId = 99;
// Act
var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, nonExistentKeyId);
// Assert
Assert.False(result);
await sutProvider.GetDependency<IUserService>().DidNotReceive()
.UpdateTwoFactorProviderAsync(Arg.Any<User>(), Arg.Any<TwoFactorProviderType>());
}
/// <summary>
/// Users must retain at least one WebAuthn credential. When only one key remains,
/// deletion should be rejected to prevent lockout.
/// </summary>
[Theory, BitAutoData]
public async Task DeleteAsync_OnlyOneKeyRemaining_ReturnsFalse(
SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)
{
// Arrange
SetupWebAuthnProvider(user, 1);
var keyIdToDelete = 1;
// Act
var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, keyIdToDelete);
// Assert
Assert.False(result);
// Key should still exist
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
Assert.NotNull(provider?.MetaData);
Assert.True(provider.MetaData.ContainsKey($"Key{keyIdToDelete}"));
await sutProvider.GetDependency<IUserService>().DidNotReceive()
.UpdateTwoFactorProviderAsync(Arg.Any<User>(), Arg.Any<TwoFactorProviderType>());
}
/// <summary>
/// When the user has no two-factor providers configured, deletion should return false.
/// </summary>
[Theory, BitAutoData]
public async Task DeleteAsync_NoProviders_ReturnsFalse(
SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)
{
// Arrange - user with no providers (clear any AutoFixture-generated ones)
user.SetTwoFactorProviders(null);
// Act
var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, 1);
// Assert
Assert.False(result);
await sutProvider.GetDependency<IUserService>().DidNotReceive()
.UpdateTwoFactorProviderAsync(Arg.Any<User>(), Arg.Any<TwoFactorProviderType>());
}
}

View File

@@ -0,0 +1,147 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Fido2NetLib;
using Fido2NetLib.Objects;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;
[SutProviderCustomize]
public class StartTwoFactorWebAuthnRegistrationCommandTests
{
private static void SetupWebAuthnProvider(User user, int credentialCount)
{
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
var metadata = new Dictionary<string, object>();
// Add credentials as Key1, Key2, Key3, etc.
for (var i = 1; i <= credentialCount; i++)
{
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
{
Name = $"Key {i}",
Descriptor = new PublicKeyCredentialDescriptor([(byte)i]),
PublicKey = [(byte)i],
UserHandle = [(byte)i],
SignatureCounter = 0,
CredType = "public-key",
RegDate = DateTime.UtcNow,
AaGuid = Guid.NewGuid()
};
}
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider { Enabled = true, MetaData = metadata };
user.SetTwoFactorProviders(providers);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task StartWebAuthnRegistrationAsync_BelowLimit_Succeeds(
bool hasPremium, SutProvider<StartTwoFactorWebAuthnRegistrationCommand> sutProvider, User user)
{
// Arrange - User should have 1 available credential, determined by maximum allowed for tested Premium status.
var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;
user.Premium = hasPremium;
user.Id = Guid.NewGuid();
user.Email = "test@example.com";
sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);
SetupWebAuthnProvider(user,
credentialCount: hasPremium
? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials - 1
: maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials - 1);
var mockFido2 = sutProvider.GetDependency<IFido2>();
mockFido2.RequestNewCredential(
Arg.Any<Fido2User>(),
Arg.Any<List<PublicKeyCredentialDescriptor>>(),
Arg.Any<AuthenticatorSelection>(),
Arg.Any<AttestationConveyancePreference>())
.Returns(new CredentialCreateOptions
{
Challenge = [1, 2, 3],
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
User = new Fido2User { Id = user.Id.ToByteArray(), Name = user.Email, DisplayName = user.Name },
PubKeyCredParams = []
});
// Act
var result = await sutProvider.Sut.StartTwoFactorWebAuthnRegistrationAsync(user);
// Assert
Assert.NotNull(result);
await sutProvider.GetDependency<IUserService>().Received(1)
.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, false);
}
/// <summary>
/// "Start" provides the first half of a two-part process for registering a new WebAuthn 2FA credential.
/// To provide the best (most aggressive) UX possible, "Start" performs boundary validation of the ability to engage
/// in this flow based on current number of configured credentials. If the user is out of available credential slots,
/// Start should throw a BadRequestException for the client to handle.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task StartWebAuthnRegistrationAsync_ExceedsLimit_ThrowsBadRequestException(
bool hasPremium, SutProvider<StartTwoFactorWebAuthnRegistrationCommand> sutProvider, User user)
{
// Arrange - User should have 1 available credential, determined by maximum allowed for tested Premium status.
var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings
{
PremiumMaximumAllowedCredentials = 10,
NonPremiumMaximumAllowedCredentials = 5
};
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;
user.Premium = hasPremium;
user.Id = Guid.NewGuid();
user.Email = "test@example.com";
sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);
SetupWebAuthnProvider(user,
credentialCount: hasPremium
? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials
: maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials);
var mockFido2 = sutProvider.GetDependency<IFido2>();
mockFido2.RequestNewCredential(
Arg.Any<Fido2User>(),
Arg.Any<List<PublicKeyCredentialDescriptor>>(),
Arg.Any<AuthenticatorSelection>(),
Arg.Any<AttestationConveyancePreference>())
.Returns(new CredentialCreateOptions
{
Challenge = [1, 2, 3],
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
User = new Fido2User { Id = user.Id.ToByteArray(), Name = user.Email, DisplayName = user.Name },
PubKeyCredParams = []
});
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.StartTwoFactorWebAuthnRegistrationAsync(user));
Assert.Equal("Maximum allowed WebAuthn credential count exceeded.", exception.Message);
}
}

View File

@@ -1,6 +1,6 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Entities;
using Bit.Core.Exceptions;

View File

@@ -812,4 +812,255 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
await _userService.Received(1).SaveUserAsync(user);
}
[Theory, BitAutoData]
public async Task Run_UserWithCanceledSubscription_AllowsResubscribe(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = true; // User still has Premium flag set
user.GatewayCustomerId = "existing_customer_123";
user.GatewaySubscriptionId = "sub_canceled_123";
paymentMethod.Type = TokenizablePaymentMethodType.Card;
paymentMethod.Token = "card_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var existingCanceledSubscription = Substitute.For<StripeSubscription>();
existingCanceledSubscription.Id = "sub_canceled_123";
existingCanceledSubscription.Status = "canceled"; // Terminal status
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "existing_customer_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var newSubscription = Substitute.For<StripeSubscription>();
newSubscription.Id = "sub_new_123";
newSubscription.Status = "active";
newSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId).Returns(existingCanceledSubscription);
_hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT0); // Should succeed, not return "Already a premium user"
Assert.True(user.Premium);
Assert.Equal(newSubscription.Id, user.GatewaySubscriptionId);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _userService.Received(1).SaveUserAsync(user);
}
[Theory, BitAutoData]
public async Task Run_UserWithIncompleteExpiredSubscription_AllowsResubscribe(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = true; // User still has Premium flag set
user.GatewayCustomerId = "existing_customer_123";
user.GatewaySubscriptionId = "sub_incomplete_expired_123";
paymentMethod.Type = TokenizablePaymentMethodType.Card;
paymentMethod.Token = "card_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var existingExpiredSubscription = Substitute.For<StripeSubscription>();
existingExpiredSubscription.Id = "sub_incomplete_expired_123";
existingExpiredSubscription.Status = "incomplete_expired"; // Terminal status
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "existing_customer_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var newSubscription = Substitute.For<StripeSubscription>();
newSubscription.Id = "sub_new_123";
newSubscription.Status = "active";
newSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId).Returns(existingExpiredSubscription);
_hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT0); // Should succeed, not return "Already a premium user"
Assert.True(user.Premium);
Assert.Equal(newSubscription.Id, user.GatewaySubscriptionId);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _userService.Received(1).SaveUserAsync(user);
}
[Theory, BitAutoData]
public async Task Run_UserWithActiveSubscription_PremiumTrue_ReturnsBadRequest(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = true;
user.GatewaySubscriptionId = "sub_active_123";
paymentMethod.Type = TokenizablePaymentMethodType.Card;
var existingActiveSubscription = Substitute.For<StripeSubscription>();
existingActiveSubscription.Id = "sub_active_123";
existingActiveSubscription.Status = "active"; // NOT a terminal status
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId).Returns(existingActiveSubscription);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT1);
var badRequest = result.AsT1;
Assert.Equal("Already a premium user.", badRequest.Response);
// Verify no subscription creation was attempted
await _stripeAdapter.DidNotReceive().CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
}
[Theory, BitAutoData]
public async Task Run_SubscriptionFetchThrows_ProceedsWithCreation(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = "existing_customer_123";
user.GatewaySubscriptionId = "sub_nonexistent_123";
paymentMethod.Type = TokenizablePaymentMethodType.Card;
paymentMethod.Token = "card_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
// Simulate Stripe exception when fetching subscription (e.g., subscription doesn't exist)
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId)
.Returns<StripeSubscription>(_ => throw new Stripe.StripeException("Subscription not found"));
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "existing_customer_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var newSubscription = Substitute.For<StripeSubscription>();
newSubscription.Id = "sub_new_123";
newSubscription.Status = "active";
newSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
_hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert - Should proceed successfully despite the exception
Assert.True(result.IsT0);
Assert.True(user.Premium);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _userService.Received(1).SaveUserAsync(user);
}
[Theory, BitAutoData]
public async Task Run_ResubscribeWithTerminalSubscription_UpdatesPaymentMethod(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = true;
user.GatewayCustomerId = "existing_customer_123";
user.GatewaySubscriptionId = "sub_canceled_123";
paymentMethod.Type = TokenizablePaymentMethodType.Card;
paymentMethod.Token = "new_card_token_456";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
var existingCanceledSubscription = Substitute.For<StripeSubscription>();
existingCanceledSubscription.Id = "sub_canceled_123";
existingCanceledSubscription.Status = "canceled"; // Terminal status
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "existing_customer_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var newSubscription = Substitute.For<StripeSubscription>();
newSubscription.Id = "sub_new_123";
newSubscription.Status = "active";
newSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
MaskedPaymentMethod mockMaskedPaymentMethod = new MaskedCard
{
Brand = "visa",
Last4 = "4567",
Expiration = "12/2026"
};
_stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId).Returns(existingCanceledSubscription);
_hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true); // Has old payment method
_updatePaymentMethodCommand.Run(Arg.Any<User>(), Arg.Any<TokenizedPaymentMethod>(), Arg.Any<BillingAddress>())
.Returns(mockMaskedPaymentMethod);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
// Assert
Assert.True(result.IsT0);
// Verify payment method was updated because of terminal subscription
await _updatePaymentMethodCommand.Received(1).Run(user, paymentMethod, billingAddress);
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _userService.Received(1).SaveUserAsync(user);
}
}

View File

@@ -31,6 +31,30 @@ public class GetBitwardenSubscriptionQueryTests
_stripeAdapter);
}
[Fact]
public async Task Run_UserWithoutGatewaySubscriptionId_ReturnsNull()
{
var user = CreateUser();
user.GatewaySubscriptionId = null;
var result = await _query.Run(user);
Assert.Null(result);
await _stripeAdapter.DidNotReceive().GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task Run_UserWithEmptyGatewaySubscriptionId_ReturnsNull()
{
var user = CreateUser();
user.GatewaySubscriptionId = string.Empty;
var result = await _query.Run(user);
Assert.Null(result);
await _stripeAdapter.DidNotReceive().GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task Run_IncompleteStatus_ReturnsBitwardenSubscriptionWithSuspension()
{

View File

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