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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>>());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user