1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 16:12:49 -05:00

Merge branch 'km/pm-10600' into km/pm-10600-full-notification-content

# Conflicts:
#	src/Core/Services/Implementations/MultiServicePushNotificationService.cs
This commit is contained in:
Maciej Zieniuk
2024-12-18 22:55:32 +00:00
365 changed files with 39468 additions and 3871 deletions

View File

@ -1,6 +1,4 @@
#nullable enable
using Bit.Admin.Models;
using Bit.Admin.Models;
using Bit.Core.Entities;
using Bit.Core.Vault.Entities;
using Bit.Test.Common.AutoFixture.Attributes;
@ -116,30 +114,26 @@ public class UserViewModelTests
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain);
Assert.True(actual.DomainVerified);
Assert.True(actual.ClaimedAccount);
}
[Theory]
[BitAutoData]
public void MapUserViewModel_WithoutVerifiedDomain_ReturnsUserViewModel(User user)
{
var verifiedDomain = false;
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain);
Assert.False(actual.DomainVerified);
Assert.False(actual.ClaimedAccount);
}
[Theory]
[BitAutoData]
public void MapUserViewModel_WithNullVerifiedDomain_ReturnsUserViewModel(User user)
{
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), null);
Assert.Null(actual.DomainVerified);
Assert.Null(actual.ClaimedAccount);
}
}

View File

@ -0,0 +1,163 @@
using System.Net;
using System.Text.Json;
using Bit.Api.AdminConsole.Public.Models.Request;
using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Test.Common.Helpers;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Public.Controllers;
public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
// These will get set in `InitializeAsync` which is ran before all tests
private Organization _organization = null!;
private string _ownerEmail = null!;
public PoliciesControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
public async Task InitializeAsync()
{
// Create the owner account
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
// Create the organization
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
// Authorize with the organization api key
await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
public async Task Post_NewPolicy()
{
var policyType = PolicyType.MasterPassword;
var request = new PolicyUpdateRequestModel
{
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minComplexity", 15},
{ "requireLower", true}
}
};
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
// Assert against the response
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();
Assert.NotNull(result);
Assert.True(result.Enabled);
Assert.Equal(policyType, result.Type);
Assert.IsType<Guid>(result.Id);
Assert.NotEqual(default, result.Id);
Assert.NotNull(result.Data);
Assert.Equal(15, ((JsonElement)result.Data["minComplexity"]).GetInt32());
Assert.True(((JsonElement)result.Data["requireLower"]).GetBoolean());
// Assert against the database values
var policyRepository = _factory.GetService<IPolicyRepository>();
var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);
Assert.NotNull(policy);
Assert.True(policy.Enabled);
Assert.Equal(policyType, policy.Type);
Assert.IsType<Guid>(policy.Id);
Assert.NotEqual(default, policy.Id);
Assert.Equal(_organization.Id, policy.OrganizationId);
Assert.NotNull(policy.Data);
var data = policy.GetDataModel<MasterPasswordPolicyData>();
var expectedData = new MasterPasswordPolicyData { MinComplexity = 15, RequireLower = true };
AssertHelper.AssertPropertyEqual(expectedData, data);
}
[Fact]
public async Task Post_UpdatePolicy()
{
var policyType = PolicyType.MasterPassword;
var existingPolicy = new Policy
{
OrganizationId = _organization.Id,
Enabled = true,
Type = policyType
};
existingPolicy.SetDataModel(new MasterPasswordPolicyData
{
EnforceOnLogin = true,
MinLength = 22,
RequireSpecial = true
});
var policyRepository = _factory.GetService<IPolicyRepository>();
await policyRepository.UpsertAsync(existingPolicy);
// The Id isn't set until it's created in the database, get it back out to get the id
var createdPolicy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);
var expectedId = createdPolicy!.Id;
var request = new PolicyUpdateRequestModel
{
Enabled = false,
Data = new Dictionary<string, object>
{
{ "minLength", 15},
{ "requireUpper", true}
}
};
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
// Assert against the response
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();
Assert.NotNull(result);
Assert.False(result.Enabled);
Assert.Equal(policyType, result.Type);
Assert.Equal(expectedId, result.Id);
Assert.NotNull(result.Data);
Assert.Equal(15, ((JsonElement)result.Data["minLength"]).GetInt32());
Assert.True(((JsonElement)result.Data["requireUpper"]).GetBoolean());
// Assert against the database values
var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);
Assert.NotNull(policy);
Assert.False(policy.Enabled);
Assert.Equal(policyType, policy.Type);
Assert.Equal(expectedId, policy.Id);
Assert.Equal(_organization.Id, policy.OrganizationId);
Assert.NotNull(policy.Data);
var data = policy.GetDataModel<MasterPasswordPolicyData>();
Assert.Equal(15, data.MinLength);
Assert.Equal(true, data.RequireUpper);
}
}

View File

@ -16,6 +16,12 @@ public class LoginHelper
_client = client;
}
public async Task LoginAsync(string email)
{
var tokens = await _factory.LoginAsync(email);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
}
public async Task LoginWithOrganizationApiKeyAsync(Guid organizationId)
{
var (clientId, apiKey) = await GetOrganizationApiKey(_factory, organizationId);

View File

@ -1,13 +1,13 @@
using System.Diagnostics;
using Bit.Api.IntegrationTest.Factories;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.IntegrationTestCommon.Factories;
namespace Bit.Api.IntegrationTest.Helpers;
@ -24,11 +24,11 @@ public static class OrganizationTestHelpers
PaymentMethodType paymentMethod = PaymentMethodType.None) where T : class
{
var userRepository = factory.GetService<IUserRepository>();
var organizationService = factory.GetService<IOrganizationService>();
var organizationSignUpCommand = factory.GetService<ICloudOrganizationSignUpCommand>();
var owner = await userRepository.GetByEmailAsync(ownerEmail);
var signUpResult = await organizationService.SignUpAsync(new OrganizationSignup
var signUpResult = await organizationSignUpCommand.SignUpOrganizationAsync(new OrganizationSignup
{
Name = name,
BillingEmail = billingEmail,
@ -39,9 +39,9 @@ public static class OrganizationTestHelpers
PaymentMethodType = paymentMethod
});
Debug.Assert(signUpResult.organizationUser is not null);
Debug.Assert(signUpResult.OrganizationUser is not null);
return new Tuple<Organization, OrganizationUser>(signUpResult.organization, signUpResult.organizationUser);
return new Tuple<Organization, OrganizationUser>(signUpResult.Organization, signUpResult.OrganizationUser);
}
/// <summary>

View File

@ -0,0 +1,164 @@
using System.Net;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.IntegrationTest.KeyManagement.Controllers;
public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private static readonly string _mockEncryptedString =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private readonly HttpClient _client;
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private readonly IUserRepository _userRepository;
private string _ownerEmail = null!;
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_factory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
"true");
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
_userRepository = _factory.GetService<IUserRepository>();
_emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>();
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
}
public async Task InitializeAsync()
{
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Theory]
[BitAutoData]
public async Task RegenerateKeysAsync_FeatureFlagTurnedOff_NotFound(KeyRegenerationRequestModel request)
{
// Localize factory to inject a false value for the feature flag.
var localFactory = new ApiApplicationFactory();
localFactory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
"false");
var localClient = localFactory.CreateClient();
var localEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
var localLoginHelper = new LoginHelper(localFactory, localClient);
await localFactory.LoginWithNewAccount(localEmail);
await localLoginHelper.LoginAsync(localEmail);
request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;
var response = await localClient.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[BitAutoData]
public async Task RegenerateKeysAsync_NotLoggedIn_Unauthorized(KeyRegenerationRequestModel request)
{
request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;
var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Theory]
[BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.Confirmed)]
[BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.RecoveryApproved)]
[BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.RecoveryInitiated)]
[BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.Confirmed)]
[BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.RecoveryApproved)]
[BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.RecoveryInitiated)]
[BitAutoData(OrganizationUserStatusType.Confirmed, null)]
[BitAutoData(OrganizationUserStatusType.Revoked, null)]
[BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.Confirmed)]
[BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.RecoveryApproved)]
[BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.RecoveryInitiated)]
public async Task RegenerateKeysAsync_UserInOrgOrHasDesignatedEmergencyAccess_ThrowsBadRequest(
OrganizationUserStatusType organizationUserStatus,
EmergencyAccessStatusType? emergencyAccessStatus,
KeyRegenerationRequestModel request)
{
if (organizationUserStatus is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked)
{
await CreateOrganizationUserAsync(organizationUserStatus);
}
if (emergencyAccessStatus != null)
{
await CreateDesignatedEmergencyAccessAsync(emergencyAccessStatus.Value);
}
await _loginHelper.LoginAsync(_ownerEmail);
request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;
var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Theory]
[BitAutoData]
public async Task RegenerateKeysAsync_Success(KeyRegenerationRequestModel request)
{
await _loginHelper.LoginAsync(_ownerEmail);
request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;
var response = await _client.PostAsJsonAsync("/accounts/key-management/regenerate-keys", request);
response.EnsureSuccessStatusCode();
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
Assert.NotNull(user);
Assert.Equal(request.UserPublicKey, user.PublicKey);
Assert.Equal(request.UserKeyEncryptedUserPrivateKey, user.PrivateKey);
}
private async Task CreateOrganizationUserAsync(OrganizationUserStatusType organizationUserStatus)
{
var (_, organizationUser) = await OrganizationTestHelpers.SignUpAsync(_factory,
PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,
paymentMethod: PaymentMethodType.Card);
organizationUser.Status = organizationUserStatus;
await _organizationUserRepository.ReplaceAsync(organizationUser);
}
private async Task CreateDesignatedEmergencyAccessAsync(EmergencyAccessStatusType emergencyAccessStatus)
{
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(tempEmail);
var tempUser = await _userRepository.GetByEmailAsync(tempEmail);
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
var emergencyAccess = new EmergencyAccess
{
GrantorId = tempUser!.Id,
GranteeId = user!.Id,
KeyEncrypted = _mockEncryptedString,
Status = emergencyAccessStatus,
Type = EmergencyAccessType.View,
WaitTimeDays = 10,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow
};
await _emergencyAccessRepository.CreateAsync(emergencyAccess);
}
}

View File

@ -0,0 +1,577 @@
using System.Net;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Response;
using Bit.Api.NotificationCenter.Models.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Repositories;
using Xunit;
namespace Bit.Api.IntegrationTest.NotificationCenter.Controllers;
public class NotificationsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private static readonly string _mockEncryptedBody =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private static readonly string _mockEncryptedTitle =
"2.06CDSJjTZaigYHUuswIq5A==|trxgZl2RCkYrrmCvGE9WNA==|w5p05eI5wsaYeSyWtsAPvBX63vj798kIMxBTfSB0BQg=";
private static readonly Random _random = new();
private static TimeSpan OneMinuteTimeSpan => TimeSpan.FromMinutes(1);
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private readonly INotificationRepository _notificationRepository;
private readonly INotificationStatusRepository _notificationStatusRepository;
private readonly IUserRepository _userRepository;
private Organization _organization = null!;
private OrganizationUser _organizationUserOwner = null!;
private string _ownerEmail = null!;
private List<(Notification, NotificationStatus?)> _notificationsWithStatuses = null!;
public NotificationsControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
_notificationRepository = _factory.GetService<INotificationRepository>();
_notificationStatusRepository = _factory.GetService<INotificationStatusRepository>();
_userRepository = _factory.GetService<IUserRepository>();
}
public async Task InitializeAsync()
{
// Create the owner account
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
// Create the organization
(_organization, _organizationUserOwner) = await OrganizationTestHelpers.SignUpAsync(_factory,
plan: PlanType.EnterpriseAnnually, ownerEmail: _ownerEmail, passwordManagerSeats: 10,
paymentMethod: PaymentMethodType.Card);
_notificationsWithStatuses = await CreateNotificationsWithStatusesAsync();
}
public Task DisposeAsync()
{
_client.Dispose();
foreach (var (notification, _) in _notificationsWithStatuses)
{
_notificationRepository.DeleteAsync(notification);
}
return Task.CompletedTask;
}
[Theory]
[InlineData("invalid")]
[InlineData("-1")]
[InlineData("0")]
public async Task ListAsync_RequestValidationContinuationInvalidNumber_BadRequest(string continuationToken)
{
await _loginHelper.LoginAsync(_ownerEmail);
var response = await _client.GetAsync($"/notifications?continuationToken={continuationToken}");
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();
Assert.NotNull(result);
Assert.Contains("ContinuationToken", result.ValidationErrors);
Assert.Contains("Continuation token must be a positive, non zero integer.",
result.ValidationErrors["ContinuationToken"]);
}
[Fact]
public async Task ListAsync_RequestValidationContinuationTokenMaxLengthExceeded_BadRequest()
{
await _loginHelper.LoginAsync(_ownerEmail);
var response = await _client.GetAsync("/notifications?continuationToken=1234567890");
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();
Assert.NotNull(result);
Assert.Contains("ContinuationToken", result.ValidationErrors);
Assert.Contains("The field ContinuationToken must be a string with a maximum length of 9.",
result.ValidationErrors["ContinuationToken"]);
}
[Theory]
[InlineData("9")]
[InlineData("1001")]
public async Task ListAsync_RequestValidationPageSizeInvalidRange_BadRequest(string pageSize)
{
await _loginHelper.LoginAsync(_ownerEmail);
var response = await _client.GetAsync($"/notifications?pageSize={pageSize}");
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();
Assert.NotNull(result);
Assert.Contains("PageSize", result.ValidationErrors);
Assert.Contains("The field PageSize must be between 10 and 1000.",
result.ValidationErrors["PageSize"]);
}
[Fact]
public async Task ListAsync_NotLoggedIn_Unauthorized()
{
var response = await _client.GetAsync("/notifications");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Theory]
[InlineData(null, null, "2", 10)]
[InlineData(10, null, "2", 10)]
[InlineData(10, 2, "3", 10)]
[InlineData(10, 3, null, 4)]
[InlineData(24, null, "2", 24)]
[InlineData(24, 2, null, 0)]
[InlineData(1000, null, null, 24)]
public async Task ListAsync_PaginationFilter_ReturnsNextPageOfNotificationsCorrectOrder(
int? pageSize, int? pageNumber, string? expectedContinuationToken, int expectedCount)
{
var pageSizeWithDefault = pageSize ?? 10;
await _loginHelper.LoginAsync(_ownerEmail);
var skip = pageNumber == null ? 0 : (pageNumber.Value - 1) * pageSizeWithDefault;
var notificationsInOrder = _notificationsWithStatuses.OrderByDescending(e => e.Item1.Priority)
.ThenByDescending(e => e.Item1.CreationDate)
.Skip(skip)
.Take(pageSizeWithDefault)
.ToList();
var url = "/notifications";
if (pageNumber != null)
{
url += $"?continuationToken={pageNumber}";
}
if (pageSize != null)
{
url += url.Contains('?') ? "&" : "?";
url += $"pageSize={pageSize}";
}
var response = await _client.GetAsync(url);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<NotificationResponseModel>>();
Assert.NotNull(result?.Data);
Assert.InRange(result.Data.Count(), 0, pageSizeWithDefault);
Assert.Equal(expectedCount, notificationsInOrder.Count);
Assert.Equal(notificationsInOrder.Count, result.Data.Count());
AssertNotificationResponseModels(result.Data, notificationsInOrder);
Assert.Equal(expectedContinuationToken, result.ContinuationToken);
}
[Theory]
[InlineData(null, null)]
[InlineData(null, false)]
[InlineData(null, true)]
[InlineData(false, null)]
[InlineData(true, null)]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public async Task ListAsync_ReadStatusDeletedStatusFilter_ReturnsFilteredNotificationsCorrectOrder(
bool? readStatusFilter, bool? deletedStatusFilter)
{
await _loginHelper.LoginAsync(_ownerEmail);
var notificationsInOrder = _notificationsWithStatuses.FindAll(e =>
(readStatusFilter == null || readStatusFilter == (e.Item2?.ReadDate != null)) &&
(deletedStatusFilter == null || deletedStatusFilter == (e.Item2?.DeletedDate != null)))
.OrderByDescending(e => e.Item1.Priority)
.ThenByDescending(e => e.Item1.CreationDate)
.Take(10)
.ToList();
var url = "/notifications";
if (readStatusFilter != null)
{
url += $"?readStatusFilter={readStatusFilter}";
}
if (deletedStatusFilter != null)
{
url += url.Contains('?') ? "&" : "?";
url += $"deletedStatusFilter={deletedStatusFilter}";
}
var response = await _client.GetAsync(url);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<NotificationResponseModel>>();
Assert.NotNull(result?.Data);
Assert.InRange(result.Data.Count(), 0, 10);
Assert.Equal(notificationsInOrder.Count, result.Data.Count());
AssertNotificationResponseModels(result.Data, notificationsInOrder);
}
[Fact]
private async void MarkAsDeletedAsync_NotLoggedIn_Unauthorized()
{
var url = $"/notifications/{Guid.NewGuid().ToString()}/delete";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
private async void MarkAsDeletedAsync_NonExistentNotificationId_NotFound()
{
await _loginHelper.LoginAsync(_ownerEmail);
var url = $"/notifications/{Guid.NewGuid()}/delete";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
private async void MarkAsDeletedAsync_UserIdNotMatching_NotFound()
{
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(email);
var user = (await _userRepository.GetByEmailAsync(email))!;
var notifications = await CreateNotificationsAsync(user.Id);
await _loginHelper.LoginAsync(_ownerEmail);
var url = $"/notifications/{notifications[0].Id}/delete";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
private async void MarkAsDeletedAsync_OrganizationIdNotMatchingUserNotPartOfOrganization_NotFound()
{
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(email);
var user = (await _userRepository.GetByEmailAsync(email))!;
var notifications = await CreateNotificationsAsync(user.Id, _organization.Id);
await _loginHelper.LoginAsync(email);
var url = $"/notifications/{notifications[0].Id}/delete";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
private async void MarkAsDeletedAsync_OrganizationIdNotMatchingUserPartOfDifferentOrganization_NotFound()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
plan: PlanType.EnterpriseAnnually, ownerEmail: _ownerEmail, passwordManagerSeats: 10,
paymentMethod: PaymentMethodType.Card);
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(email);
var user = (await _userRepository.GetByEmailAsync(email))!;
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, email, OrganizationUserType.User);
var notifications = await CreateNotificationsAsync(user.Id, _organization.Id);
await _loginHelper.LoginAsync(email);
var url = $"/notifications/{notifications[0].Id}/delete";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
private async void MarkAsDeletedAsync_NotificationStatusNotExisting_Created()
{
var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId);
await _loginHelper.LoginAsync(_ownerEmail);
var url = $"/notifications/{notifications[0].Id}/delete";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(
notifications[0].Id, _organizationUserOwner.UserId!.Value);
Assert.NotNull(notificationStatus);
Assert.NotNull(notificationStatus.DeletedDate);
Assert.Equal(DateTime.UtcNow, notificationStatus.DeletedDate.Value, OneMinuteTimeSpan);
Assert.Null(notificationStatus.ReadDate);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
private async void MarkAsDeletedAsync_NotificationStatusExisting_Updated(bool deletedDateNull)
{
var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId);
await _notificationStatusRepository.CreateAsync(new NotificationStatus
{
NotificationId = notifications[0].Id,
UserId = _organizationUserOwner.UserId!.Value,
ReadDate = null,
DeletedDate = deletedDateNull ? null : DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600))
});
await _loginHelper.LoginAsync(_ownerEmail);
var url = $"/notifications/{notifications[0].Id}/delete";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(
notifications[0].Id, _organizationUserOwner.UserId!.Value);
Assert.NotNull(notificationStatus);
Assert.NotNull(notificationStatus.DeletedDate);
Assert.Equal(DateTime.UtcNow, notificationStatus.DeletedDate.Value, OneMinuteTimeSpan);
Assert.Null(notificationStatus.ReadDate);
}
[Fact]
private async void MarkAsReadAsync_NotLoggedIn_Unauthorized()
{
var url = $"/notifications/{Guid.NewGuid().ToString()}/read";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
private async void MarkAsReadAsync_NonExistentNotificationId_NotFound()
{
await _loginHelper.LoginAsync(_ownerEmail);
var url = $"/notifications/{Guid.NewGuid()}/read";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
private async void MarkAsReadAsync_UserIdNotMatching_NotFound()
{
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(email);
var user = (await _userRepository.GetByEmailAsync(email))!;
var notifications = await CreateNotificationsAsync(user.Id);
await _loginHelper.LoginAsync(_ownerEmail);
var url = $"/notifications/{notifications[0].Id}/read";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
private async void MarkAsReadAsync_OrganizationIdNotMatchingUserNotPartOfOrganization_NotFound()
{
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(email);
var user = (await _userRepository.GetByEmailAsync(email))!;
var notifications = await CreateNotificationsAsync(user.Id, _organization.Id);
await _loginHelper.LoginAsync(email);
var url = $"/notifications/{notifications[0].Id}/read";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
private async void MarkAsReadAsync_OrganizationIdNotMatchingUserPartOfDifferentOrganization_NotFound()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
plan: PlanType.EnterpriseAnnually, ownerEmail: _ownerEmail, passwordManagerSeats: 10,
paymentMethod: PaymentMethodType.Card);
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(email);
var user = (await _userRepository.GetByEmailAsync(email))!;
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, email, OrganizationUserType.User);
var notifications = await CreateNotificationsAsync(user.Id, _organization.Id);
await _loginHelper.LoginAsync(email);
var url = $"/notifications/{notifications[0].Id}/read";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
private async void MarkAsReadAsync_NotificationStatusNotExisting_Created()
{
var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId);
await _loginHelper.LoginAsync(_ownerEmail);
var url = $"/notifications/{notifications[0].Id}/read";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(
notifications[0].Id, _organizationUserOwner.UserId!.Value);
Assert.NotNull(notificationStatus);
Assert.NotNull(notificationStatus.ReadDate);
Assert.Equal(DateTime.UtcNow, notificationStatus.ReadDate.Value, OneMinuteTimeSpan);
Assert.Null(notificationStatus.DeletedDate);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
private async void MarkAsReadAsync_NotificationStatusExisting_Updated(bool readDateNull)
{
var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId);
await _notificationStatusRepository.CreateAsync(new NotificationStatus
{
NotificationId = notifications[0].Id,
UserId = _organizationUserOwner.UserId!.Value,
ReadDate = readDateNull ? null : DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)),
DeletedDate = null
});
await _loginHelper.LoginAsync(_ownerEmail);
var url = $"/notifications/{notifications[0].Id}/read";
var response = await _client.PatchAsync(url, new StringContent(""));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(
notifications[0].Id, _organizationUserOwner.UserId!.Value);
Assert.NotNull(notificationStatus);
Assert.NotNull(notificationStatus.ReadDate);
Assert.Equal(DateTime.UtcNow, notificationStatus.ReadDate.Value, OneMinuteTimeSpan);
Assert.Null(notificationStatus.DeletedDate);
}
private static void AssertNotificationResponseModels(
IEnumerable<NotificationResponseModel> notificationResponseModels,
List<(Notification, NotificationStatus?)> expectedNotificationsWithStatuses)
{
var i = 0;
foreach (var notificationResponseModel in notificationResponseModels)
{
Assert.Contains(expectedNotificationsWithStatuses, e => e.Item1.Id == notificationResponseModel.Id);
var (expectedNotification, expectedNotificationStatus) = expectedNotificationsWithStatuses[i];
Assert.NotNull(expectedNotification);
Assert.Equal(expectedNotification.Priority, notificationResponseModel.Priority);
Assert.Equal(expectedNotification.Title, notificationResponseModel.Title);
Assert.Equal(expectedNotification.Body, notificationResponseModel.Body);
Assert.Equal(expectedNotification.RevisionDate, notificationResponseModel.Date);
if (expectedNotificationStatus != null)
{
Assert.Equal(expectedNotificationStatus.ReadDate, notificationResponseModel.ReadDate);
Assert.Equal(expectedNotificationStatus.DeletedDate, notificationResponseModel.DeletedDate);
}
else
{
Assert.Null(notificationResponseModel.ReadDate);
Assert.Null(notificationResponseModel.DeletedDate);
}
Assert.Equal("notification", notificationResponseModel.Object);
i++;
}
}
private async Task<List<(Notification, NotificationStatus?)>> CreateNotificationsWithStatusesAsync()
{
var userId = (Guid)_organizationUserOwner.UserId!;
var globalNotifications = await CreateNotificationsAsync();
var userWithoutOrganizationNotifications = await CreateNotificationsAsync(userId: userId);
var organizationWithoutUserNotifications = await CreateNotificationsAsync(organizationId: _organization.Id);
var userPartOrOrganizationNotifications = await CreateNotificationsAsync(userId: userId,
organizationId: _organization.Id);
var globalNotificationWithStatuses = await CreateNotificationStatusesAsync(globalNotifications, userId);
var userWithoutOrganizationNotificationWithStatuses =
await CreateNotificationStatusesAsync(userWithoutOrganizationNotifications, userId);
var organizationWithoutUserNotificationWithStatuses =
await CreateNotificationStatusesAsync(organizationWithoutUserNotifications, userId);
var userPartOrOrganizationNotificationWithStatuses =
await CreateNotificationStatusesAsync(userPartOrOrganizationNotifications, userId);
return new List<List<(Notification, NotificationStatus?)>>
{
globalNotificationWithStatuses,
userWithoutOrganizationNotificationWithStatuses,
organizationWithoutUserNotificationWithStatuses,
userPartOrOrganizationNotificationWithStatuses
}
.SelectMany(n => n)
.Where(n => n.Item1.ClientType is ClientType.All or ClientType.Web)
.ToList();
}
private async Task<List<Notification>> CreateNotificationsAsync(Guid? userId = null, Guid? organizationId = null,
int numberToCreate = 3)
{
var priorities = Enum.GetValues<Priority>();
var clientTypes = Enum.GetValues<ClientType>();
var notifications = new List<Notification>();
foreach (var clientType in clientTypes)
{
for (var i = 0; i < numberToCreate; i++)
{
var notification = new Notification
{
Global = userId == null && organizationId == null,
UserId = userId,
OrganizationId = organizationId,
Title = _mockEncryptedTitle,
Body = _mockEncryptedBody,
Priority = (Priority)priorities.GetValue(_random.Next(priorities.Length))!,
ClientType = clientType,
CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)),
RevisionDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600))
};
notification = await _notificationRepository.CreateAsync(notification);
notifications.Add(notification);
}
}
return notifications;
}
private async Task<List<(Notification, NotificationStatus?)>> CreateNotificationStatusesAsync(
List<Notification> notifications, Guid userId)
{
var readDateNotificationStatus = await _notificationStatusRepository.CreateAsync(new NotificationStatus
{
NotificationId = notifications[0].Id,
UserId = userId,
ReadDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)),
DeletedDate = null
});
var deletedDateNotificationStatus = await _notificationStatusRepository.CreateAsync(new NotificationStatus
{
NotificationId = notifications[1].Id,
UserId = userId,
ReadDate = null,
DeletedDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600))
});
var readDateAndDeletedDateNotificationStatus = await _notificationStatusRepository.CreateAsync(
new NotificationStatus
{
NotificationId = notifications[2].Id,
UserId = userId,
ReadDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)),
DeletedDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600))
});
List<NotificationStatus> statuses =
[readDateNotificationStatus, deletedDateNotificationStatus, readDateAndDeletedDateNotificationStatus];
return notifications.Select(n => (n, statuses.Find(s => s.NotificationId == n.Id))).ToList();
}
}

View File

@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
@ -46,11 +47,11 @@ public class OrganizationsControllerTests : IDisposable
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
private readonly IFeatureService _featureService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IProviderRepository _providerRepository;
private readonly IProviderBillingService _providerBillingService;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly OrganizationsController _sut;
public OrganizationsControllerTests()
@ -69,11 +70,11 @@ public class OrganizationsControllerTests : IDisposable
_userService = Substitute.For<IUserService>();
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
_featureService = Substitute.For<IFeatureService>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_providerRepository = Substitute.For<IProviderRepository>();
_providerBillingService = Substitute.For<IProviderBillingService>();
_orgDeleteTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<OrgDeleteTokenable>>();
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
_cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>();
_sut = new OrganizationsController(
_organizationRepository,
@ -90,11 +91,11 @@ public class OrganizationsControllerTests : IDisposable
_organizationApiKeyRepository,
_featureService,
_globalSettings,
_pushNotificationService,
_providerRepository,
_providerBillingService,
_orgDeleteTokenDataFactory,
_removeOrganizationUserCommand);
_removeOrganizationUserCommand,
_cloudOrganizationSignUpCommand);
}
public void Dispose()
@ -129,7 +130,7 @@ public class OrganizationsControllerTests : IDisposable
Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.",
exception.Message);
await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default);
await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().UserLeaveAsync(default, default);
}
[Theory, AutoData]
@ -192,7 +193,7 @@ public class OrganizationsControllerTests : IDisposable
await _sut.Leave(orgId);
await _removeOrganizationUserCommand.Received(1).RemoveUserAsync(orgId, user.Id);
await _removeOrganizationUserCommand.Received(1).UserLeaveAsync(orgId, user.Id);
}
[Theory, AutoData]

View File

@ -1,33 +0,0 @@
using Bit.Api.AdminConsole.Public.Controllers;
using Bit.Api.AdminConsole.Public.Models.Request;
using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Mvc;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Public.Controllers;
[ControllerCustomize(typeof(PoliciesController))]
[SutProviderCustomize]
public class PoliciesControllerTests
{
[Theory]
[BitAutoData]
[BitAutoData(PolicyType.SendOptions)]
public async Task Put_NewPolicy_AppliesCorrectType(PolicyType type, Organization organization, PolicyUpdateRequestModel model, SutProvider<PoliciesController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationId.Returns(organization.Id);
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(organization.Id, type).Returns((Policy)null);
var response = await sutProvider.Sut.Put(type, model) as JsonResult;
var responseValue = response.Value as PolicyResponseModel;
Assert.Equal(type, responseValue.Type);
}
}

View File

@ -4,7 +4,7 @@ using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.Auth.Validators;
using Bit.Api.KeyManagement.Validators;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;
using Bit.Core;
@ -14,12 +14,12 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;

View File

@ -10,6 +10,7 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -47,6 +48,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IReferenceEventService _referenceEventService;
private readonly ISubscriberService _subscriberService;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IOrganizationInstallationRepository _organizationInstallationRepository;
private readonly OrganizationsController _sut;
@ -70,6 +72,7 @@ public class OrganizationsControllerTests : IDisposable
_referenceEventService = Substitute.For<IReferenceEventService>();
_subscriberService = Substitute.For<ISubscriberService>();
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
_organizationInstallationRepository = Substitute.For<IOrganizationInstallationRepository>();
_sut = new OrganizationsController(
_organizationRepository,
@ -85,7 +88,8 @@ public class OrganizationsControllerTests : IDisposable
_upgradeOrganizationPlanCommand,
_addSecretsManagerSubscriptionCommand,
_referenceEventService,
_subscriberService);
_subscriberService,
_organizationInstallationRepository);
}
public void Dispose()

View File

@ -6,11 +6,13 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tokens;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -28,10 +30,19 @@ public class PoliciesControllerTests
[Theory]
[BitAutoData]
public async Task GetMasterPasswordPolicy_WhenCalled_ReturnsMasterPasswordPolicy(
SutProvider<PoliciesController> sutProvider, Guid orgId, Guid userId, OrganizationUser orgUser,
Policy policy, MasterPasswordPolicyData mpPolicyData)
SutProvider<PoliciesController> sutProvider,
Guid orgId, Guid userId,
OrganizationUser orgUser,
Policy policy,
MasterPasswordPolicyData mpPolicyData,
Organization organization)
{
// Arrange
organization.UsePolicies = true;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(orgId).Returns(organization);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns((Guid?)userId);
@ -135,6 +146,39 @@ public class PoliciesControllerTests
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));
}
[Theory]
[BitAutoData]
public async Task GetMasterPasswordPolicy_WhenUsePoliciesIsFalse_ThrowsNotFoundException(
SutProvider<PoliciesController> sutProvider,
Guid orgId)
{
// Arrange
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(orgId).Returns((Organization)null);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));
}
[Theory]
[BitAutoData]
public async Task GetMasterPasswordPolicy_WhenOrgIsNull_ThrowsNotFoundException(
SutProvider<PoliciesController> sutProvider,
Guid orgId,
Organization organization)
{
// Arrange
organization.UsePolicies = false;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(orgId).Returns(organization);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));
}
[Theory]
[BitAutoData]
public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExistingPolicy(
@ -142,16 +186,16 @@ public class PoliciesControllerTests
{
// Arrange
sutProvider.GetDependency<ICurrentContext>()
.ManagePolicies(orgId)
.Returns(true);
.ManagePolicies(orgId)
.Returns(true);
policy.Type = (PolicyType)type;
policy.Enabled = true;
policy.Data = null;
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
.Returns(policy);
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
.Returns(policy);
// Act
var result = await sutProvider.Sut.Get(orgId, type);
@ -171,12 +215,12 @@ public class PoliciesControllerTests
{
// Arrange
sutProvider.GetDependency<ICurrentContext>()
.ManagePolicies(orgId)
.Returns(true);
.ManagePolicies(orgId)
.Returns(true);
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
.Returns((Policy)null);
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
.Returns((Policy)null);
// Act
var result = await sutProvider.Sut.Get(orgId, type);
@ -194,11 +238,221 @@ public class PoliciesControllerTests
{
// Arrange
sutProvider.GetDependency<ICurrentContext>()
.ManagePolicies(orgId)
.Returns(false);
.ManagePolicies(orgId)
.Returns(false);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Get(orgId, type));
}
[Theory]
[BitAutoData]
public async Task GetByToken_WhenOrganizationUseUsePoliciesIsFalse_ThrowsNotFoundException(
SutProvider<PoliciesController> sutProvider, Guid orgId, Guid organizationUserId, string token, string email,
Organization organization)
{
// Arrange
organization.UsePolicies = false;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(orgId).Returns(organization);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId));
}
[Theory]
[BitAutoData]
public async Task GetByToken_WhenOrganizationIsNull_ThrowsNotFoundException(
SutProvider<PoliciesController> sutProvider, Guid orgId, Guid organizationUserId, string token, string email)
{
// Arrange
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(orgId).Returns((Organization)null);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId));
}
[Theory]
[BitAutoData]
public async Task GetByToken_WhenTokenIsInvalid_ThrowsNotFoundException(
SutProvider<PoliciesController> sutProvider,
Guid orgId,
Guid organizationUserId,
string token,
string email,
Organization organization
)
{
// Arrange
organization.UsePolicies = true;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(orgId).Returns(organization);
var decryptedToken = Substitute.For<OrgUserInviteTokenable>();
decryptedToken.Valid.Returns(false);
var orgUserInviteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>();
orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any<OrgUserInviteTokenable>())
.Returns(x =>
{
x[1] = decryptedToken;
return true;
});
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId));
}
[Theory]
[BitAutoData]
public async Task GetByToken_WhenUserIsNull_ThrowsNotFoundException(
SutProvider<PoliciesController> sutProvider,
Guid orgId,
Guid organizationUserId,
string token,
string email,
Organization organization
)
{
// Arrange
organization.UsePolicies = true;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(orgId).Returns(organization);
var decryptedToken = Substitute.For<OrgUserInviteTokenable>();
decryptedToken.Valid.Returns(true);
decryptedToken.OrgUserId = organizationUserId;
decryptedToken.OrgUserEmail = email;
var orgUserInviteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>();
orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any<OrgUserInviteTokenable>())
.Returns(x =>
{
x[1] = decryptedToken;
return true;
});
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId)
.Returns((OrganizationUser)null);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId));
}
[Theory]
[BitAutoData]
public async Task GetByToken_WhenUserOrgIdDoesNotMatchOrgId_ThrowsNotFoundException(
SutProvider<PoliciesController> sutProvider,
Guid orgId,
Guid organizationUserId,
string token,
string email,
OrganizationUser orgUser,
Organization organization
)
{
// Arrange
organization.UsePolicies = true;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(orgId).Returns(organization);
var decryptedToken = Substitute.For<OrgUserInviteTokenable>();
decryptedToken.Valid.Returns(true);
decryptedToken.OrgUserId = organizationUserId;
decryptedToken.OrgUserEmail = email;
var orgUserInviteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>();
orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any<OrgUserInviteTokenable>())
.Returns(x =>
{
x[1] = decryptedToken;
return true;
});
orgUser.OrganizationId = Guid.Empty;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId)
.Returns(orgUser);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId));
}
[Theory]
[BitAutoData]
public async Task GetByToken_ShouldReturnEnabledPolicies(
SutProvider<PoliciesController> sutProvider,
Guid orgId,
Guid organizationUserId,
string token,
string email,
OrganizationUser orgUser,
Organization organization
)
{
// Arrange
organization.UsePolicies = true;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(orgId).Returns(organization);
var decryptedToken = Substitute.For<OrgUserInviteTokenable>();
decryptedToken.Valid.Returns(true);
decryptedToken.OrgUserId = organizationUserId;
decryptedToken.OrgUserEmail = email;
var orgUserInviteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>();
orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any<OrgUserInviteTokenable>())
.Returns(x =>
{
x[1] = decryptedToken;
return true;
});
orgUser.OrganizationId = orgId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId)
.Returns(orgUser);
var enabledPolicy = Substitute.For<Policy>();
enabledPolicy.Enabled = true;
var disabledPolicy = Substitute.For<Policy>();
disabledPolicy.Enabled = false;
var policies = new[] { enabledPolicy, disabledPolicy };
sutProvider.GetDependency<IPolicyRepository>()
.GetManyByOrganizationIdAsync(orgId)
.Returns(policies);
// Act
var result = await sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId);
// Assert
var expectedPolicy = result.Data.Single();
Assert.NotNull(result);
Assert.Equal(enabledPolicy.Id, expectedPolicy.Id);
Assert.Equal(enabledPolicy.Type, expectedPolicy.Type);
Assert.Equal(enabledPolicy.Enabled, expectedPolicy.Enabled);
}
}

View File

@ -0,0 +1,96 @@
#nullable enable
using System.Security.Claims;
using Bit.Api.KeyManagement.Controllers;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Commands.Interfaces;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Api.Test.KeyManagement.Controllers;
[ControllerCustomize(typeof(AccountsKeyManagementController))]
[SutProviderCustomize]
[JsonDocumentCustomize]
public class AccountsKeyManagementControllerTests
{
[Theory]
[BitAutoData]
public async Task RegenerateKeysAsync_FeatureFlagOff_Throws(
SutProvider<AccountsKeyManagementController> sutProvider,
KeyRegenerationRequestModel data)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration))
.Returns(false);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(data));
await sutProvider.GetDependency<IOrganizationUserRepository>().ReceivedWithAnyArgs(0)
.GetManyByUserAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IEmergencyAccessRepository>().ReceivedWithAnyArgs(0)
.GetManyDetailsByGranteeIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().ReceivedWithAnyArgs(0)
.RegenerateKeysAsync(Arg.Any<UserAsymmetricKeys>(),
Arg.Any<ICollection<OrganizationUser>>(),
Arg.Any<ICollection<EmergencyAccessDetails>>());
}
[Theory]
[BitAutoData]
public async Task RegenerateKeysAsync_UserNull_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
KeyRegenerationRequestModel data)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration))
.Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.RegenerateKeysAsync(data));
await sutProvider.GetDependency<IOrganizationUserRepository>().ReceivedWithAnyArgs(0)
.GetManyByUserAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IEmergencyAccessRepository>().ReceivedWithAnyArgs(0)
.GetManyDetailsByGranteeIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().ReceivedWithAnyArgs(0)
.RegenerateKeysAsync(Arg.Any<UserAsymmetricKeys>(),
Arg.Any<ICollection<OrganizationUser>>(),
Arg.Any<ICollection<EmergencyAccessDetails>>());
}
[Theory]
[BitAutoData]
public async Task RegenerateKeysAsync_Success(SutProvider<AccountsKeyManagementController> sutProvider,
KeyRegenerationRequestModel data, User user, ICollection<OrganizationUser> orgUsers,
ICollection<EmergencyAccessDetails> accessDetails)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Is(FeatureFlagKeys.PrivateKeyRegeneration))
.Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(Arg.Is(user.Id)).Returns(orgUsers);
sutProvider.GetDependency<IEmergencyAccessRepository>().GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id))
.Returns(accessDetails);
await sutProvider.Sut.RegenerateKeysAsync(data);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.GetManyByUserAsync(Arg.Is(user.Id));
await sutProvider.GetDependency<IEmergencyAccessRepository>().Received(1)
.GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id));
await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().Received(1)
.RegenerateKeysAsync(
Arg.Is<UserAsymmetricKeys>(u =>
u.UserId == user.Id && u.PublicKey == data.UserPublicKey &&
u.UserKeyEncryptedPrivateKey == data.UserKeyEncryptedUserPrivateKey),
Arg.Is(orgUsers),
Arg.Is(accessDetails));
}
}

View File

@ -1,5 +1,5 @@
using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Validators;
using Bit.Api.KeyManagement.Validators;
using Bit.Api.Vault.Models.Request;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Vault.Models.Data;
@ -9,7 +9,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Vault.Validators;
namespace Bit.Api.Test.KeyManagement.Validators;
[SutProviderCustomize]
public class CipherRotationValidatorTests

View File

@ -1,5 +1,5 @@
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Validators;
using Bit.Api.KeyManagement.Validators;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
@ -10,7 +10,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Auth.Validators;
namespace Bit.Api.Test.KeyManagement.Validators;
[SutProviderCustomize]
public class EmergencyAccessRotationValidatorTests

View File

@ -1,5 +1,5 @@
using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Validators;
using Bit.Api.KeyManagement.Validators;
using Bit.Api.Vault.Models.Request;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Vault.Entities;
@ -9,7 +9,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Vault.Validators;
namespace Bit.Api.Test.KeyManagement.Validators;
[SutProviderCustomize]
public class FolderRotationValidatorTests

View File

@ -1,5 +1,5 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Validators;
using Bit.Api.KeyManagement.Validators;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
@ -8,7 +8,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Validators;
namespace Bit.Api.Test.KeyManagement.Validators;
[SutProviderCustomize]
public class OrganizationUserRotationValidatorTests

View File

@ -1,7 +1,7 @@
using System.Text.Json;
using Bit.Api.KeyManagement.Validators;
using Bit.Api.Tools.Models;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Tools.Validators;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Tools.Entities;
@ -14,7 +14,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Tools.Validators;
namespace Bit.Api.Test.KeyManagement.Validators;
[SutProviderCustomize]
public class SendRotationValidatorTests

View File

@ -1,5 +1,5 @@
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.Auth.Validators;
using Bit.Api.KeyManagement.Validators;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
@ -9,7 +9,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Auth.Validators;
namespace Bit.Api.Test.KeyManagement.Validators;
[SutProviderCustomize]
public class WebAuthnLoginKeyRotationValidatorTests

View File

@ -0,0 +1,202 @@
#nullable enable
using Bit.Api.NotificationCenter.Controllers;
using Bit.Api.NotificationCenter.Models.Request;
using Bit.Core.Models.Data;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Models.Data;
using Bit.Core.NotificationCenter.Models.Filter;
using Bit.Core.NotificationCenter.Queries.Interfaces;
using Bit.Core.Test.NotificationCenter.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.NotificationCenter.Controllers;
[ControllerCustomize(typeof(NotificationsController))]
[SutProviderCustomize]
public class NotificationsControllerTests
{
[Theory]
[BitAutoData([null, null])]
[BitAutoData([null, false])]
[BitAutoData([null, true])]
[BitAutoData(false, null)]
[BitAutoData(true, null)]
[BitAutoData(false, false)]
[BitAutoData(false, true)]
[BitAutoData(true, false)]
[BitAutoData(true, true)]
[NotificationStatusDetailsListCustomize(5)]
public async Task ListAsync_StatusFilter_ReturnedMatchingNotifications(bool? readStatusFilter, bool? deletedStatusFilter,
SutProvider<NotificationsController> sutProvider,
IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)
{
var notificationStatusDetailsList = notificationStatusDetailsEnumerable
.OrderByDescending(n => n.Priority)
.ThenByDescending(n => n.CreationDate)
.ToList();
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())
.Returns(new PagedResult<NotificationStatusDetails> { Data = notificationStatusDetailsList });
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
.Take(10)
.ToDictionary(n => n.Id);
var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel
{
ReadStatusFilter = readStatusFilter,
DeletedStatusFilter = deletedStatusFilter
});
Assert.Equal("list", listResponse.Object);
Assert.Equal(5, listResponse.Data.Count());
Assert.All(listResponse.Data, notificationResponseModel =>
{
Assert.Equal("notification", notificationResponseModel.Object);
Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id));
var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id];
Assert.NotNull(expectedNotificationStatusDetails);
Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id);
Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority);
Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title);
Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body);
Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date);
Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);
Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);
});
Assert.Null(listResponse.ContinuationToken);
await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.Received(1)
.GetByUserIdStatusFilterAsync(Arg.Is<NotificationStatusFilter>(filter =>
filter.Read == readStatusFilter && filter.Deleted == deletedStatusFilter),
Arg.Is<PageOptions>(pageOptions =>
pageOptions.ContinuationToken == null && pageOptions.PageSize == 10));
}
[Theory]
[BitAutoData]
[NotificationStatusDetailsListCustomize(19)]
public async Task ListAsync_PagingRequestNoContinuationToken_ReturnedFirst10MatchingNotifications(
SutProvider<NotificationsController> sutProvider,
IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)
{
var notificationStatusDetailsList = notificationStatusDetailsEnumerable
.OrderByDescending(n => n.Priority)
.ThenByDescending(n => n.CreationDate)
.ToList();
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())
.Returns(new PagedResult<NotificationStatusDetails>
{ Data = notificationStatusDetailsList.Take(10).ToList(), ContinuationToken = "2" });
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
.Take(10)
.ToDictionary(n => n.Id);
var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel());
Assert.Equal("list", listResponse.Object);
Assert.Equal(10, listResponse.Data.Count());
Assert.All(listResponse.Data, notificationResponseModel =>
{
Assert.Equal("notification", notificationResponseModel.Object);
Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id));
var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id];
Assert.NotNull(expectedNotificationStatusDetails);
Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id);
Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority);
Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title);
Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body);
Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date);
Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);
Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);
});
Assert.Equal("2", listResponse.ContinuationToken);
await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.Received(1)
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(),
Arg.Is<PageOptions>(pageOptions =>
pageOptions.ContinuationToken == null && pageOptions.PageSize == 10));
}
[Theory]
[BitAutoData]
[NotificationStatusDetailsListCustomize(19)]
public async Task ListAsync_PagingRequestUsingContinuationToken_ReturnedLast9MatchingNotifications(
SutProvider<NotificationsController> sutProvider,
IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)
{
var notificationStatusDetailsList = notificationStatusDetailsEnumerable
.OrderByDescending(n => n.Priority)
.ThenByDescending(n => n.CreationDate)
.ToList();
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())
.Returns(new PagedResult<NotificationStatusDetails>
{ Data = notificationStatusDetailsList.Skip(10).ToList() });
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
.Skip(10)
.ToDictionary(n => n.Id);
var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel { ContinuationToken = "2" });
Assert.Equal("list", listResponse.Object);
Assert.Equal(9, listResponse.Data.Count());
Assert.All(listResponse.Data, notificationResponseModel =>
{
Assert.Equal("notification", notificationResponseModel.Object);
Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id));
var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id];
Assert.NotNull(expectedNotificationStatusDetails);
Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id);
Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority);
Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title);
Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body);
Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date);
Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);
Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);
});
Assert.Null(listResponse.ContinuationToken);
await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.Received(1)
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(),
Arg.Is<PageOptions>(pageOptions =>
pageOptions.ContinuationToken == "2" && pageOptions.PageSize == 10));
}
[Theory]
[BitAutoData]
public async Task MarkAsDeletedAsync_NotificationId_MarkedAsDeleted(
SutProvider<NotificationsController> sutProvider,
Guid notificationId)
{
await sutProvider.Sut.MarkAsDeletedAsync(notificationId);
await sutProvider.GetDependency<IMarkNotificationDeletedCommand>()
.Received(1)
.MarkDeletedAsync(notificationId);
}
[Theory]
[BitAutoData]
public async Task MarkAsReadAsync_NotificationId_MarkedAsRead(
SutProvider<NotificationsController> sutProvider,
Guid notificationId)
{
await sutProvider.Sut.MarkAsReadAsync(notificationId);
await sutProvider.GetDependency<IMarkNotificationReadCommand>()
.Received(1)
.MarkReadAsync(notificationId);
}
}

View File

@ -0,0 +1,93 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Api.NotificationCenter.Models.Request;
using Xunit;
namespace Bit.Api.Test.NotificationCenter.Models.Request;
public class NotificationFilterRequestModelTests
{
[Theory]
[InlineData("invalid")]
[InlineData("-1")]
[InlineData("0")]
public void Validate_ContinuationTokenInvalidNumber_Invalid(string continuationToken)
{
var model = new NotificationFilterRequestModel
{
ContinuationToken = continuationToken,
};
var result = Validate(model);
Assert.Single(result);
Assert.Contains("Continuation token must be a positive, non zero integer.", result[0].ErrorMessage);
Assert.Contains("ContinuationToken", result[0].MemberNames);
}
[Fact]
public void Validate_ContinuationTokenMaxLengthExceeded_Invalid()
{
var model = new NotificationFilterRequestModel
{
ContinuationToken = "1234567890"
};
var result = Validate(model);
Assert.Single(result);
Assert.Contains("The field ContinuationToken must be a string with a maximum length of 9.",
result[0].ErrorMessage);
Assert.Contains("ContinuationToken", result[0].MemberNames);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("1")]
[InlineData("123456789")]
public void Validate_ContinuationTokenCorrect_Valid(string? continuationToken)
{
var model = new NotificationFilterRequestModel
{
ContinuationToken = continuationToken
};
var result = Validate(model);
Assert.Empty(result);
}
[Theory]
[InlineData(9)]
[InlineData(1001)]
public void Validate_PageSizeInvalidRange_Invalid(int pageSize)
{
var model = new NotificationFilterRequestModel
{
PageSize = pageSize
};
var result = Validate(model);
Assert.Single(result);
Assert.Contains("The field PageSize must be between 10 and 1000.", result[0].ErrorMessage);
Assert.Contains("PageSize", result[0].MemberNames);
}
[Theory]
[InlineData(null)]
[InlineData(10)]
[InlineData(1000)]
public void Validate_PageSizeCorrect_Valid(int? pageSize)
{
var model = pageSize == null
? new NotificationFilterRequestModel()
: new NotificationFilterRequestModel
{
PageSize = pageSize.Value
};
var result = Validate(model);
Assert.Empty(result);
}
private static List<ValidationResult> Validate(NotificationFilterRequestModel model)
{
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, new ValidationContext(model), results, true);
return results;
}
}

View File

@ -0,0 +1,43 @@
#nullable enable
using Bit.Api.NotificationCenter.Models.Response;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.NotificationCenter.Models.Data;
using Xunit;
namespace Bit.Api.Test.NotificationCenter.Models.Response;
public class NotificationResponseModelTests
{
[Fact]
public void Constructor_NotificationStatusDetailsNull_CorrectFields()
{
Assert.Throws<ArgumentNullException>(() => new NotificationResponseModel(null!));
}
[Fact]
public void Constructor_NotificationStatusDetails_CorrectFields()
{
var notificationStatusDetails = new NotificationStatusDetails
{
Id = Guid.NewGuid(),
Global = true,
Priority = Priority.High,
ClientType = ClientType.All,
Title = "Test Title",
Body = "Test Body",
RevisionDate = DateTime.UtcNow - TimeSpan.FromMinutes(3),
ReadDate = DateTime.UtcNow - TimeSpan.FromMinutes(1),
DeletedDate = DateTime.UtcNow,
};
var model = new NotificationResponseModel(notificationStatusDetails);
Assert.Equal(model.Id, notificationStatusDetails.Id);
Assert.Equal(model.Priority, notificationStatusDetails.Priority);
Assert.Equal(model.Title, notificationStatusDetails.Title);
Assert.Equal(model.Body, notificationStatusDetails.Body);
Assert.Equal(model.Date, notificationStatusDetails.RevisionDate);
Assert.Equal(model.ReadDate, notificationStatusDetails.ReadDate);
Assert.Equal(model.DeletedDate, notificationStatusDetails.DeletedDate);
}
}

View File

@ -0,0 +1,95 @@
using System.Security.Claims;
using Bit.Api.Tools.Authorization;
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Test.AdminConsole.Helpers;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Tools.Authorization;
[SutProviderCustomize]
public class VaultExportAuthorizationHandlerTests
{
public static IEnumerable<object[]> CanExportWholeVault => new List<CurrentContextOrganization>
{
new () { Type = OrganizationUserType.Owner },
new () { Type = OrganizationUserType.Admin },
new ()
{
Type = OrganizationUserType.Custom, Permissions = new Permissions { AccessImportExport = true }
}
}.Select(org => new[] { org });
[Theory]
[BitMemberAutoData(nameof(CanExportWholeVault))]
public async Task ExportAll_PermittedRoles_Success(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user,
SutProvider<VaultExportAuthorizationHandler> sutProvider)
{
org.Id = orgScope;
sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgScope).Returns(org);
var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportWholeVault }, user, orgScope);
await sutProvider.Sut.HandleAsync(authContext);
Assert.True(authContext.HasSucceeded);
}
public static IEnumerable<object[]> CannotExportWholeVault => new List<CurrentContextOrganization>
{
new () { Type = OrganizationUserType.User },
new ()
{
Type = OrganizationUserType.Custom, Permissions = new Permissions { AccessImportExport = true }.Invert()
}
}.Select(org => new[] { org });
[Theory]
[BitMemberAutoData(nameof(CannotExportWholeVault))]
public async Task ExportAll_NotPermitted_Failure(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user,
SutProvider<VaultExportAuthorizationHandler> sutProvider)
{
org.Id = orgScope;
sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgScope).Returns(org);
var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportWholeVault }, user, orgScope);
await sutProvider.Sut.HandleAsync(authContext);
Assert.False(authContext.HasSucceeded);
}
public static IEnumerable<object[]> CanExportManagedCollections =>
AuthorizationHelpers.AllRoles().Select(o => new[] { o });
[Theory]
[BitMemberAutoData(nameof(CanExportManagedCollections))]
public async Task ExportManagedCollections_PermittedRoles_Success(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user,
SutProvider<VaultExportAuthorizationHandler> sutProvider)
{
org.Id = orgScope;
sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgScope).Returns(org);
var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportManagedCollections }, user, orgScope);
await sutProvider.Sut.HandleAsync(authContext);
Assert.True(authContext.HasSucceeded);
}
[Theory]
[BitAutoData([null])]
public async Task ExportManagedCollections_NotPermitted_Failure(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user,
SutProvider<VaultExportAuthorizationHandler> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgScope).Returns(org);
var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportManagedCollections }, user, orgScope);
await sutProvider.Sut.HandleAsync(authContext);
Assert.False(authContext.HasSucceeded);
}
}

View File

@ -1,7 +1,9 @@
using Bit.Api.Tools.Controllers;
using AutoFixture;
using Bit.Api.Tools.Controllers;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Tools.ReportFeatures.Interfaces;
using Bit.Core.Tools.ReportFeatures.Requests;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -45,5 +47,98 @@ public class ReportsControllerTests
.Received(0);
}
[Theory, BitAutoData]
public async Task AddPasswordHealthReportApplicationAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var request = new Api.Tools.Models.PasswordHealthReportApplicationModel
{
OrganizationId = Guid.NewGuid(),
Url = "https://example.com",
};
await sutProvider.Sut.AddPasswordHealthReportApplication(request);
// Assert
_ = sutProvider.GetDependency<IAddPasswordHealthReportApplicationCommand>()
.Received(1)
.AddPasswordHealthReportApplicationAsync(Arg.Is<AddPasswordHealthReportApplicationRequest>(_ =>
_.OrganizationId == request.OrganizationId && _.Url == request.Url));
}
[Theory, BitAutoData]
public async Task AddPasswordHealthReportApplicationAsync_multiple_withAccess_success(
SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var fixture = new Fixture();
var request = fixture.CreateMany<Api.Tools.Models.PasswordHealthReportApplicationModel>(2);
await sutProvider.Sut.AddPasswordHealthReportApplications(request);
// Assert
_ = sutProvider.GetDependency<IAddPasswordHealthReportApplicationCommand>()
.Received(1)
.AddPasswordHealthReportApplicationAsync(Arg.Any<IEnumerable<AddPasswordHealthReportApplicationRequest>>());
}
[Theory, BitAutoData]
public async Task AddPasswordHealthReportApplicationAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var request = new Api.Tools.Models.PasswordHealthReportApplicationModel
{
OrganizationId = Guid.NewGuid(),
Url = "https://example.com",
};
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.AddPasswordHealthReportApplication(request));
// Assert
_ = sutProvider.GetDependency<IAddPasswordHealthReportApplicationCommand>()
.Received(0);
}
[Theory, BitAutoData]
public async Task DropPasswordHealthReportApplicationAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var fixture = new Fixture();
var request = fixture.Create<Api.Tools.Models.PasswordHealthReportApplicationModel>();
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.AddPasswordHealthReportApplication(request));
// Assert
_ = sutProvider.GetDependency<IDropPasswordHealthReportApplicationCommand>()
.Received(0);
}
[Theory, BitAutoData]
public async Task DropPasswordHealthReportApplicationAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var fixture = new Fixture();
var request = fixture.Create<DropPasswordHealthReportApplicationRequest>();
await sutProvider.Sut.DropPasswordHealthReportApplication(request);
// Assert
_ = sutProvider.GetDependency<IDropPasswordHealthReportApplicationCommand>()
.Received(1)
.DropPasswordHealthReportApplicationAsync(Arg.Is<DropPasswordHealthReportApplicationRequest>(_ =>
_.OrganizationId == request.OrganizationId &&
_.PasswordHealthReportApplicationIds == request.PasswordHealthReportApplicationIds));
}
}

View File

@ -1,6 +1,5 @@
using System.Security.Claims;
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -33,10 +32,7 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType;
organization.Permissions = new Permissions();
// `LimitCollectonCreationDeletionSplit` feature flag state isn't
// relevant for this test. The flag is never checked for in this
// test. This is asserted below.
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
ArrangeOrganizationAbility(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create },
@ -48,12 +44,11 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections,
CurrentContextOrganization organization)
@ -62,7 +57,7 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = OrganizationUserType.User;
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false);
ArrangeOrganizationAbility(sutProvider, organization, false, false);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create },
@ -71,49 +66,16 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
.Returns(false);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = OrganizationUserType.User;
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create },
new ClaimsPrincipal(),
collections);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
.Returns(true);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
public async Task CanCreateAsync_WhenMissingPermissions_NoSuccess(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections,
@ -130,7 +92,7 @@ public class BulkCollectionAuthorizationHandlerTests
ManageUsers = false
};
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
ArrangeOrganizationAbility(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create },
@ -140,61 +102,21 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = userType;
organization.Permissions = new Permissions
{
EditAnyCollection = false,
DeleteAnyCollection = false,
ManageGroups = false,
ManageUsers = false
};
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create },
new ClaimsPrincipal(),
collections);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitDisabled_NoSuccess(
public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess(
Guid userId,
CurrentContextOrganization organization,
List<Collection> collections,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
{
collections.ForEach(c => c.OrganizationId = organization.Id);
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
ArrangeOrganizationAbility(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create },
@ -205,38 +127,9 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitEnabled_NoSuccess(
Guid userId,
CurrentContextOrganization organization,
List<Collection> collections,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
{
collections.ForEach(c => c.OrganizationId = organization.Id);
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create },
new ClaimsPrincipal(),
collections
);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
@ -1015,7 +908,7 @@ public class BulkCollectionAuthorizationHandlerTests
// `LimitCollectonCreationDeletionSplit` feature flag state isn't
// relevant for this test. The flag is never checked for in this
// test. This is asserted below.
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
ArrangeOrganizationAbility(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
@ -1027,7 +920,6 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
@ -1046,7 +938,7 @@ public class BulkCollectionAuthorizationHandlerTests
// `LimitCollectonCreationDeletionSplit` feature flag state isn't
// relevant for this test. The flag is never checked for in this
// test. This is asserted below.
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
ArrangeOrganizationAbility(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
@ -1058,12 +950,11 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
@ -1073,12 +964,11 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false);
ArrangeOrganizationAbility(sutProvider, organization, false, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
foreach (var c in collections)
{
@ -1092,41 +982,6 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
foreach (var c in collections)
{
c.Manage = true;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
@ -1134,7 +989,7 @@ public class BulkCollectionAuthorizationHandlerTests
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.User)]
public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
@ -1145,12 +1000,11 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false, false);
ArrangeOrganizationAbility(sutProvider, organization, false, false, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
foreach (var c in collections)
{
@ -1164,15 +1018,13 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.User)]
public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
@ -1183,12 +1035,11 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false, false);
ArrangeOrganizationAbility(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
foreach (var c in collections)
{
@ -1202,14 +1053,13 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_Failure(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
@ -1220,87 +1070,12 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
foreach (var c in collections)
{
c.Manage = true;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = userType;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
foreach (var c in collections)
{
c.Manage = true;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = userType;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
ArrangeOrganizationAbility(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
foreach (var c in collections)
{
@ -1314,50 +1089,11 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = userType;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
foreach (var c in collections)
{
c.Manage = false;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_Failure(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
@ -1367,13 +1103,12 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
ArrangeOrganizationAbility(sutProvider, organization, true, true);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
foreach (var c in collections)
{
@ -1387,12 +1122,11 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_Failure(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
@ -1402,13 +1136,12 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
ArrangeOrganizationAbility(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
foreach (var c in collections)
{
@ -1422,88 +1155,13 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
.Returns(false);
foreach (var c in collections)
{
c.Manage = true;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
.Returns(true);
foreach (var c in collections)
{
c.Manage = true;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
public async Task CanDeleteAsync_WhenMissingPermissions_NoSuccess(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections,
@ -1520,7 +1178,7 @@ public class BulkCollectionAuthorizationHandlerTests
ManageUsers = false
};
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
ArrangeOrganizationAbility(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
@ -1530,54 +1188,14 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = userType;
organization.Permissions = new Permissions
{
EditAnyCollection = false,
DeleteAnyCollection = false,
ManageGroups = false,
ManageUsers = false
};
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
public async Task CanDeleteAsync_WhenMissingOrgAccess_NoSuccess(
Guid userId,
ICollection<Collection> collections,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
@ -1591,34 +1209,9 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
Guid userId,
ICollection<Collection> collections,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
{
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections
);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
@ -1639,7 +1232,6 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasFailed);
sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs();
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
}
[Theory, BitAutoData, CollectionCustomization]
@ -1663,66 +1255,10 @@ public class BulkCollectionAuthorizationHandlerTests
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.HandleAsync(context));
Assert.Equal("Requested collections must belong to the same organization.", exception.Message);
sutProvider.GetDependency<ICurrentContext>().DidNotReceiveWithAnyArgs().GetOrganization(default);
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections)
{
var actingUserId = Guid.NewGuid();
var orgId = collections.First().OrganizationId;
var organizationAbilities = new Dictionary<Guid, OrganizationAbility>
{
{ collections.First().OrganizationId,
new OrganizationAbility
{
LimitCollectionCreationDeletion = true,
AllowAdminAccessToAllCollectionItems = true
}
}
};
var operationsToTest = new[]
{
BulkCollectionOperations.Create,
BulkCollectionOperations.Read,
BulkCollectionOperations.ReadAccess,
BulkCollectionOperations.Update,
BulkCollectionOperations.ModifyUserAccess,
BulkCollectionOperations.ModifyGroupAccess,
BulkCollectionOperations.Delete,
};
foreach (var op in operationsToTest)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgId).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
.Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
var context = new AuthorizationHandlerContext(
new[] { op },
new ClaimsPrincipal(),
collections
);
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
await sutProvider.GetDependency<ICurrentContext>().Received().ProviderUserForOrgAsync(orgId);
// Recreate the SUT to reset the mocks/dependencies between tests
sutProvider.Recreate();
}
}
[Theory, BitAutoData, CollectionCustomization]
public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
public async Task HandleRequirementAsync_Provider_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections)
{
@ -1759,7 +1295,6 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
.Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
var context = new AuthorizationHandlerContext(
new[] { op },
@ -1810,30 +1345,12 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(Arg.Any<Guid>());
}
private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
CurrentContextOrganization organization,
bool limitCollectionCreation,
bool limitCollectionDeletion,
bool allowAdminAccessToAllCollectionItems = true)
{
var organizationAbility = new OrganizationAbility();
organizationAbility.Id = organization.Id;
organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreation || limitCollectionDeletion;
organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems;
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
}
private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
CurrentContextOrganization organization,
bool limitCollectionCreation,
bool limitCollectionDeletion,
bool allowAdminAccessToAllCollectionItems = true)
private static void ArrangeOrganizationAbility(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
CurrentContextOrganization organization,
bool limitCollectionCreation,
bool limitCollectionDeletion,
bool allowAdminAccessToAllCollectionItems = true)
{
var organizationAbility = new OrganizationAbility();
organizationAbility.Id = organization.Id;

View File

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Divergic.Logging.Xunit" Version="4.3.0" />
<PackageReference Include="FluentAssertions" Version="6.12.1" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageReference Include="xunit" Version="$(XUnitVersion)" />

View File

@ -2,6 +2,7 @@
using AutoFixture;
using AutoFixture.Xunit2;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
namespace Bit.Core.Test.AdminConsole.AutoFixture;
@ -12,7 +13,8 @@ internal class PolicyUpdateCustomization(PolicyType type, bool enabled) : ICusto
{
fixture.Customize<PolicyUpdate>(composer => composer
.With(o => o.Type, type)
.With(o => o.Enabled, enabled));
.With(o => o.Enabled, enabled)
.With(o => o.PerformedBy, new StandardUser(Guid.NewGuid(), false)));
}
}

View File

@ -0,0 +1,52 @@
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Core.Test.AdminConsole.Helpers;
public static class AuthorizationHelpers
{
/// <summary>
/// Return a new Permission object with inverted permissions.
/// This is useful to test negative cases, e.g. "all other permissions should fail".
/// </summary>
/// <param name="permissions"></param>
/// <returns></returns>
public static Permissions Invert(this Permissions permissions)
{
// Get all false boolean properties of input object
var inputsToFlip = permissions
.GetType()
.GetProperties()
.Where(p =>
p.PropertyType == typeof(bool) &&
(bool)p.GetValue(permissions, null)! == false)
.Select(p => p.Name);
var result = new Permissions();
// Set these to true on the result object
result
.GetType()
.GetProperties()
.Where(p => inputsToFlip.Contains(p.Name))
.ToList()
.ForEach(p => p.SetValue(result, true));
return result;
}
/// <summary>
/// Returns a sequence of all possible roles and permissions represented as CurrentContextOrganization objects.
/// Used largely for authorization testing.
/// </summary>
/// <returns></returns>
public static IEnumerable<CurrentContextOrganization> AllRoles() => new List<CurrentContextOrganization>
{
new () { Type = OrganizationUserType.Owner },
new () { Type = OrganizationUserType.Admin },
new () { Type = OrganizationUserType.Custom, Permissions = new Permissions() },
new () { Type = OrganizationUserType.Custom, Permissions = new Permissions().Invert() },
new () { Type = OrganizationUserType.User },
};
}

View File

@ -0,0 +1,38 @@
using Bit.Core.Models.Data;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Helpers;
public class AuthorizationHelpersTests
{
[Fact]
public void Permissions_Invert_InvertsAllPermissions()
{
var sut = new Permissions
{
AccessEventLogs = true,
AccessReports = true,
DeleteAnyCollection = true,
ManagePolicies = true,
ManageScim = true
};
var result = sut.Invert();
Assert.True(result is
{
AccessEventLogs: false,
AccessImportExport: true,
AccessReports: false,
CreateNewCollections: true,
EditAnyCollection: true,
DeleteAnyCollection: false,
ManageGroups: true,
ManagePolicies: false,
ManageSso: true,
ManageUsers: true,
ManageResetPassword: true,
ManageScim: false
});
}
}

View File

@ -1,10 +1,15 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
@ -28,7 +33,12 @@ public class VerifyOrganizationDomainCommandTests
DomainName = "Test Domain",
Txt = "btw+test18383838383"
};
sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(Guid.NewGuid());
expected.SetVerifiedDate();
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetByIdAsync(id)
.Returns(expected);
@ -53,6 +63,10 @@ public class VerifyOrganizationDomainCommandTests
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetByIdAsync(id)
.Returns(expected);
sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(Guid.NewGuid());
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(expected.DomainName)
.Returns(new List<OrganizationDomain> { expected });
@ -77,9 +91,14 @@ public class VerifyOrganizationDomainCommandTests
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetByIdAsync(id)
.Returns(expected);
sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(Guid.NewGuid());
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(expected.DomainName)
.Returns(new List<OrganizationDomain>());
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(expected.DomainName, Arg.Any<string>())
.Returns(true);
@ -107,9 +126,14 @@ public class VerifyOrganizationDomainCommandTests
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetByIdAsync(id)
.Returns(expected);
sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(Guid.NewGuid());
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(expected.DomainName)
.Returns(new List<OrganizationDomain>());
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(expected.DomainName, Arg.Any<string>())
.Returns(false);
@ -143,7 +167,7 @@ public class VerifyOrganizationDomainCommandTests
[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled(
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
OrganizationDomain domain, Guid userId, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
@ -157,11 +181,18 @@ public class VerifyOrganizationDomainCommandTests
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(userId);
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>()
await sutProvider.GetDependency<ISavePolicyCommand>()
.Received(1)
.SaveAsync(Arg.Is<Policy>(x => x.Type == PolicyType.SingleOrg && x.OrganizationId == domain.OrganizationId && x.Enabled), null);
.SaveAsync(Arg.Is<PolicyUpdate>(x => x.Type == PolicyType.SingleOrg &&
x.OrganizationId == domain.OrganizationId &&
x.Enabled &&
x.PerformedBy is StandardUser &&
x.PerformedBy.UserId == userId));
}
[Theory, BitAutoData]
@ -176,20 +207,23 @@ public class VerifyOrganizationDomainCommandTests
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(Guid.NewGuid());
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(false);
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>()
await sutProvider.GetDependency<ISavePolicyCommand>()
.DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null);
.SaveAsync(Arg.Any<PolicyUpdate>());
}
[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled(
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
@ -199,16 +233,18 @@ public class VerifyOrganizationDomainCommandTests
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(false);
sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(Guid.NewGuid());
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>()
await sutProvider.GetDependency<ISavePolicyCommand>()
.DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null);
.SaveAsync(Arg.Any<PolicyUpdate>());
}
[Theory, BitAutoData]
@ -223,14 +259,66 @@ public class VerifyOrganizationDomainCommandTests
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(false);
sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(Guid.NewGuid());
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IPolicyService>()
await sutProvider.GetDependency<ISavePolicyCommand>()
.DidNotReceive()
.SaveAsync(Arg.Any<Policy>(), null);
.SaveAsync(Arg.Any<PolicyUpdate>());
}
[Theory, BitAutoData]
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenEmailShouldBeSentToUsersWhoBelongToTheDomain(
ICollection<OrganizationUserUserDetails> organizationUsers,
OrganizationDomain domain,
Organization organization,
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
foreach (var organizationUser in organizationUsers)
{
organizationUser.Email = $"{organizationUser.Name}@{domain.DomainName}";
}
var mockedUsers = organizationUsers
.Where(x => x.Status != OrganizationUserStatusType.Invited &&
x.Status != OrganizationUserStatusType.Revoked).ToList();
organization.Id = domain.OrganizationId;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
.Returns([]);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(domain.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IDnsResolverService>()
.ResolveAsync(domain.DomainName, domain.Txt)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.UserId.Returns(Guid.NewGuid());
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(domain.OrganizationId)
.Returns(mockedUsers);
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IMailService>().Received().SendClaimedDomainUserEmailAsync(
Arg.Is<ManagedUserDomainClaimedEmails>(x =>
x.EmailList.Count(e => e.EndsWith(domain.DomainName)) == mockedUsers.Count &&
x.Organization.Id == organization.Id));
}
}

View File

@ -258,14 +258,15 @@ public class DeleteManagedOrganizationUserAccountCommandTests
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, true } });
// Act
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUser1.Id, orgUser2.Id }, null);
var userIds = new[] { orgUser1.Id, orgUser2.Id };
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, userIds, null);
// Assert
Assert.Equal(2, results.Count());
Assert.All(results, r => Assert.Empty(r.Item2));
await sutProvider.GetDependency<IUserService>().Received(1).DeleteAsync(user1);
await sutProvider.GetDependency<IUserService>().Received(1).DeleteAsync(user2);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyAsync(userIds);
await sutProvider.GetDependency<IUserRepository>().Received(1).DeleteManyAsync(Arg.Is<IEnumerable<User>>(users => users.Any(u => u.Id == user1.Id) && users.Any(u => u.Id == user2.Id)));
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(
Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1
@ -286,7 +287,9 @@ public class DeleteManagedOrganizationUserAccountCommandTests
Assert.Single(result);
Assert.Equal(orgUserId, result.First().Item1);
Assert.Contains("Member not found.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IUserRepository>()
.DidNotReceiveWithAnyArgs()
.DeleteManyAsync(default);
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
@ -484,7 +487,6 @@ public class DeleteManagedOrganizationUserAccountCommandTests
Assert.Equal("You cannot delete a member with Invited status.", results.First(r => r.Item1 == orgUser2.Id).Item2);
Assert.Equal("Member is not managed by the organization.", results.First(r => r.Item1 == orgUser3.Id).Item2);
await sutProvider.GetDependency<IUserService>().Received(1).DeleteAsync(user1);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(
Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1));

View File

@ -0,0 +1,185 @@
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
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 RevokeNonCompliantOrganizationUserCommandTests
{
[Theory, BitAutoData]
public async Task RevokeNonCompliantOrganizationUsersAsync_GivenUnrecognizedUserType_WhenAttemptingToRevoke_ThenErrorShouldBeReturned(
Guid organizationId, SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)
{
var command = new RevokeOrganizationUsersRequest(organizationId, [], new InvalidUser());
var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);
Assert.True(result.HasErrors);
Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorRequestedByWasNotValid, result.ErrorMessages);
}
[Theory, BitAutoData]
public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeThemselves_ThenErrorShouldBeReturned(
Guid organizationId, OrganizationUserUserDetails revokingUser,
SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)
{
var command = new RevokeOrganizationUsersRequest(organizationId, revokingUser,
new StandardUser(revokingUser?.UserId ?? Guid.NewGuid(), true));
var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);
Assert.True(result.HasErrors);
Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorCannotRevokeSelf, result.ErrorMessages);
}
[Theory, BitAutoData]
public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeOrgUsersFromAnotherOrg_ThenErrorShouldBeReturned(
Guid organizationId, OrganizationUserUserDetails userFromAnotherOrg,
SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)
{
userFromAnotherOrg.OrganizationId = Guid.NewGuid();
var command = new RevokeOrganizationUsersRequest(organizationId, userFromAnotherOrg,
new StandardUser(Guid.NewGuid(), true));
var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);
Assert.True(result.HasErrors);
Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorInvalidUsers, result.ErrorMessages);
}
[Theory, BitAutoData]
public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeAllOwnersFromOrg_ThenErrorShouldBeReturned(
Guid organizationId, OrganizationUserUserDetails userToRevoke,
SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)
{
userToRevoke.OrganizationId = organizationId;
var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke,
new StandardUser(Guid.NewGuid(), true));
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(false);
var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);
Assert.True(result.HasErrors);
Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorOrgMustHaveAtLeastOneOwner, result.ErrorMessages);
}
[Theory, BitAutoData]
public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeOwnerWhenNotAnOwner_ThenErrorShouldBeReturned(
Guid organizationId, OrganizationUserUserDetails userToRevoke,
SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)
{
userToRevoke.OrganizationId = organizationId;
userToRevoke.Type = OrganizationUserType.Owner;
var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke,
new StandardUser(Guid.NewGuid(), false));
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);
Assert.True(result.HasErrors);
Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorOnlyOwnersCanRevokeOtherOwners, result.ErrorMessages);
}
[Theory, BitAutoData]
public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeUserWhoIsAlreadyRevoked_ThenErrorShouldBeReturned(
Guid organizationId, OrganizationUserUserDetails userToRevoke,
SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)
{
userToRevoke.OrganizationId = organizationId;
userToRevoke.Status = OrganizationUserStatusType.Revoked;
var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke,
new StandardUser(Guid.NewGuid(), true));
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);
Assert.True(result.HasErrors);
Assert.Contains($"{RevokeNonCompliantOrganizationUserCommand.ErrorUserAlreadyRevoked} Id: {userToRevoke.Id}", result.ErrorMessages);
}
[Theory, BitAutoData]
public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserHasMultipleInvalidUsers_ThenErrorShouldBeReturned(
Guid organizationId, IEnumerable<OrganizationUserUserDetails> usersToRevoke,
SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)
{
var revocableUsers = usersToRevoke.ToList();
revocableUsers.ForEach(user => user.OrganizationId = organizationId);
revocableUsers[0].Type = OrganizationUserType.Owner;
revocableUsers[1].Status = OrganizationUserStatusType.Revoked;
var command = new RevokeOrganizationUsersRequest(organizationId, revocableUsers,
new StandardUser(Guid.NewGuid(), false));
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);
Assert.True(result.HasErrors);
Assert.True(result.ErrorMessages.Count > 1);
}
[Theory, BitAutoData]
public async Task RevokeNonCompliantOrganizationUsersAsync_GivenValidPopulatedRequest_WhenUserAttemptsToRevokeAUser_ThenUserShouldBeRevoked(
Guid organizationId, OrganizationUserUserDetails userToRevoke,
SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)
{
userToRevoke.OrganizationId = organizationId;
userToRevoke.Type = OrganizationUserType.Admin;
var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke,
new StandardUser(Guid.NewGuid(), false));
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.RevokeManyByIdAsync(Arg.Any<IEnumerable<Guid>>());
Assert.True(result.Success);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventsAsync(
Arg.Is<IEnumerable<(OrganizationUserUserDetails organizationUser, EventType eventType, DateTime? time
)>>(
x => x.Any(y =>
y.organizationUser.Id == userToRevoke.Id && y.eventType == EventType.OrganizationUser_Revoked)
));
}
public class InvalidUser : IActingUser
{
public Guid? UserId => Guid.Empty;
public bool IsOrganizationOwnerOrProvider => false;
public EventSystemUser? SystemUserType => null;
}
}

View File

@ -14,34 +14,13 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
public class UpdateOrganizationUserGroupsCommandTests
{
[Theory, BitAutoData]
public async Task UpdateUserGroups_Passes(
public async Task UpdateUserGroups_ShouldUpdateUserGroupsAndLogUserEvent(
OrganizationUser organizationUser,
IEnumerable<Guid> groupIds,
SutProvider<UpdateOrganizationUserGroupsCommand> sutProvider)
{
await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds, null);
await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds);
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs()
.ValidateOrganizationUserUpdatePermissions(default, default, default, default);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.UpdateGroupsAsync(organizationUser.Id, groupIds);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);
}
[Theory, BitAutoData]
public async Task UpdateUserGroups_WithSavingUserId_Passes(
OrganizationUser organizationUser,
IEnumerable<Guid> groupIds,
Guid savingUserId,
SutProvider<UpdateOrganizationUserGroupsCommand> sutProvider)
{
organizationUser.Permissions = null;
await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds, savingUserId);
await sutProvider.GetDependency<IOrganizationService>().Received(1)
.ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, null, organizationUser.GetPermissions());
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.UpdateGroupsAsync(organizationUser.Id, groupIds);
await sutProvider.GetDependency<IEventService>().Received(1)

View File

@ -0,0 +1,238 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.StaticStore;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp;
[SutProviderCustomize]
public class CloudICloudOrganizationSignUpCommandTests
{
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.Plan = planType;
var plan = StaticStore.GetPlan(signup.Plan);
signup.AdditionalSeats = 0;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
signup.IsFromSecretsManagerTrial = false;
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(
Arg.Is<Organization>(o =>
o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats
&& o.SmSeats == null
&& o.SmServiceAccounts == null));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
referenceEvent.Type == ReferenceEventType.Signup &&
referenceEvent.PlanName == plan.Name &&
referenceEvent.PlanType == plan.Type &&
referenceEvent.Seats == result.Organization.Seats &&
referenceEvent.Storage == result.Organization.MaxStorageGb));
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
Assert.NotNull(result.Organization);
Assert.NotNull(result.OrganizationUser);
await sutProvider.GetDependency<IPaymentService>().Received(1).PurchaseOrganizationAsync(
Arg.Any<Organization>(),
signup.PaymentMethodType.Value,
signup.PaymentToken,
plan,
signup.AdditionalStorageGb,
signup.AdditionalSeats,
signup.PremiumAccessAddon,
signup.TaxInfo,
false,
signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(),
signup.UseSecretsManager
);
}
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task SignUp_AssignsOwnerToDefaultCollection
(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.Plan = planType;
signup.AdditionalSeats = 0;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
// Extract orgUserId when created
Guid? orgUserId = null;
await sutProvider.GetDependency<IOrganizationUserRepository>()
.CreateAsync(Arg.Do<OrganizationUser>(ou => orgUserId = ou.Id));
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
// Assert: created a Can Manage association for the default collection
Assert.NotNull(orgUserId);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).CreateAsync(
Arg.Any<Collection>(),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cas => cas == null),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cas =>
cas.Count() == 1 &&
cas.All(c =>
c.Id == orgUserId &&
!c.ReadOnly &&
!c.HidePasswords &&
c.Manage)));
Assert.NotNull(result.Organization);
Assert.NotNull(result.OrganizationUser);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsMonthly)]
public async Task SignUp_SM_Passes(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.Plan = planType;
var plan = StaticStore.GetPlan(signup.Plan);
signup.UseSecretsManager = true;
signup.AdditionalSeats = 15;
signup.AdditionalSmSeats = 10;
signup.AdditionalServiceAccounts = 20;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.IsFromSecretsManagerTrial = false;
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(
Arg.Is<Organization>(o =>
o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats
&& o.SmSeats == plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats
&& o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + signup.AdditionalServiceAccounts));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
referenceEvent.Type == ReferenceEventType.Signup &&
referenceEvent.PlanName == plan.Name &&
referenceEvent.PlanType == plan.Type &&
referenceEvent.Seats == result.Organization.Seats &&
referenceEvent.Storage == result.Organization.MaxStorageGb));
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
Assert.NotNull(result.Organization);
Assert.NotNull(result.OrganizationUser);
await sutProvider.GetDependency<IPaymentService>().Received(1).PurchaseOrganizationAsync(
Arg.Any<Organization>(),
signup.PaymentMethodType.Value,
signup.PaymentToken,
Arg.Is<Plan>(plan),
signup.AdditionalStorageGb,
signup.AdditionalSeats,
signup.PremiumAccessAddon,
signup.TaxInfo,
false,
signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(),
signup.IsFromSecretsManagerTrial
);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task SignUp_SM_Throws_WhenManagedByMSP(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.Plan = planType;
signup.UseSecretsManager = true;
signup.AdditionalSeats = 15;
signup.AdditionalSmSeats = 10;
signup.AdditionalServiceAccounts = 20;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.IsFromProvider = true;
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_SecretManager_AdditionalServiceAccounts_NotAllowedByPlan_ShouldThrowException(OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.AdditionalSmSeats = 0;
signup.AdditionalSeats = 0;
signup.Plan = PlanType.Free;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
signup.AdditionalStorageGb = 0;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_SMSeatsGreatThanPMSeat_ShouldThrowException(OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.AdditionalSmSeats = 100;
signup.AdditionalSeats = 10;
signup.Plan = PlanType.EnterpriseAnnually;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_InvalidateServiceAccount_ShouldThrowException(OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.AdditionalSmSeats = 10;
signup.AdditionalSeats = 10;
signup.Plan = PlanType.EnterpriseAnnually;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = -10;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("You can't subtract Machine Accounts!", exception.Message);
}
}

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.Auth.Entities;
@ -10,6 +11,7 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Commands;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -61,6 +63,92 @@ public class SingleOrgPolicyValidatorTests
Assert.True(string.IsNullOrEmpty(result));
}
[Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_RevokesNonCompliantUsers(
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg, false)] Policy policy,
Guid savingUserId,
Guid nonCompliantUserId,
Organization organization, SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
var compliantUser1 = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = new Guid(),
Email = "user1@example.com"
};
var compliantUser2 = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = new Guid(),
Email = "user2@example.com"
};
var nonCompliantUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = nonCompliantUserId,
Email = "user3@example.com"
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([compliantUser1, compliantUser2, nonCompliantUser]);
var otherOrganizationUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = new Guid(),
UserId = nonCompliantUserId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(nonCompliantUserId)))
.Returns([otherOrganizationUser]);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
.Returns(new CommandResult());
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(
Arg.Is<RevokeOrganizationUsersRequest>(r =>
r.OrganizationId == organization.Id &&
r.OrganizationUsers.Count() == 1 &&
r.OrganizationUsers.First().Id == nonCompliantUser.Id));
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email);
}
[Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers(
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
@ -71,6 +159,94 @@ public class SingleOrgPolicyValidatorTests
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
var compliantUser1 = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = new Guid(),
Email = "user1@example.com"
};
var compliantUser2 = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = new Guid(),
Email = "user2@example.com"
};
var nonCompliantUser = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Confirmed,
UserId = nonCompliantUserId,
Email = "user3@example.com"
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([compliantUser1, compliantUser2, nonCompliantUser]);
var otherOrganizationUser = new OrganizationUser
{
Id = Guid.NewGuid(),
OrganizationId = new Guid(),
UserId = nonCompliantUserId,
Status = OrganizationUserStatusType.Confirmed
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(nonCompliantUserId)))
.Returns([otherOrganizationUser]);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(false);
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
.Returns(new CommandResult());
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
.DidNotReceive()
.RemoveUserAsync(policyUpdate.OrganizationId, compliantUser1.Id, savingUserId);
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
.DidNotReceive()
.RemoveUserAsync(policyUpdate.OrganizationId, compliantUser2.Id, savingUserId);
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
.Received(1)
.RemoveUserAsync(policyUpdate.OrganizationId, nonCompliantUser.Id, savingUserId);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email);
}
[Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_WhenAccountDeprovisioningIsEnabled_ThenUsersAreRevoked(
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
[Policy(PolicyType.SingleOrg, false)] Policy policy,
Guid savingUserId,
Guid nonCompliantUserId,
Organization organization, SutProvider<SingleOrgPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
var compliantUser1 = new OrganizationUserUserDetails
{
OrganizationId = organization.Id,
@ -114,16 +290,19 @@ public class SingleOrgPolicyValidatorTests
.Returns([otherOrganizationUser]);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId)
.Returns(organization);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
.Returns(new CommandResult());
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
.Received(1)
.RemoveUserAsync(policyUpdate.OrganizationId, nonCompliantUser.Id, savingUserId);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(),
"user3@example.com");
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());
}
}

View File

@ -1,12 +1,14 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Commands;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -176,6 +178,10 @@ public class TwoFactorAuthenticationPolicyValidatorTests
HasMasterPassword = false
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(false);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policy.OrganizationId)
.Returns(new List<OrganizationUserUserDetails>
@ -201,9 +207,151 @@ public class TwoFactorAuthenticationPolicyValidatorTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy));
Assert.Contains("Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, badRequestException.Message);
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>().DidNotReceiveWithAnyArgs()
.RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default);
}
[Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_GivenUpdateTo2faPolicy_WhenAccountProvisioningIsDisabled_ThenRevokeUserCommandShouldNotBeCalled(
Organization organization,
[PolicyUpdate(PolicyType.TwoFactorAuthentication)]
PolicyUpdate policyUpdate,
[Policy(PolicyType.TwoFactorAuthentication, false)]
Policy policy,
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(false);
var orgUserDetailUserAcceptedWithout2Fa = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Accepted,
Type = OrganizationUserType.User,
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns(new List<OrganizationUserUserDetails>
{
orgUserDetailUserAcceptedWithout2Fa
});
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(orgUserDetailUserAcceptedWithout2Fa, false),
});
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.DidNotReceive()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());
}
[Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_GivenUpdateTo2faPolicy_WhenAccountProvisioningIsEnabledAndUserDoesNotHaveMasterPassword_ThenNonCompliantMembersErrorMessageWillReturn(
Organization organization,
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([orgUserDetailUserWithout2Fa]);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(orgUserDetailUserWithout2Fa, false),
});
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy));
Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, exception.Message);
}
[Theory, BitAutoData]
public async Task OnSaveSideEffectsAsync_WhenAccountProvisioningIsEnabledAndUserHasMasterPassword_ThenUserWillBeRevoked(
Organization organization,
[PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,
[Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,
SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)
{
policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)
.Returns([orgUserDetailUserWithout2Fa]);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(orgUserDetailUserWithout2Fa, true),
});
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())
.Returns(new CommandResult());
await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(),
"user3@test.com");
}
}

View File

@ -100,7 +100,7 @@ public class SavePolicyCommandTests
}
[Theory, BitAutoData]
public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest(PolicyUpdate policyUpdate)
public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate)
{
var sutProvider = SutProviderFactory();
sutProvider.GetDependency<IApplicationCacheService>()
@ -115,7 +115,7 @@ public class SavePolicyCommandTests
}
[Theory, BitAutoData]
public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest(PolicyUpdate policyUpdate)
public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate)
{
var sutProvider = SutProviderFactory();
sutProvider.GetDependency<IApplicationCacheService>()

View File

@ -169,7 +169,6 @@ public class EventServiceTests
new EventMessage()
{
IpAddress = ipAddress,
DeviceType = DeviceType.Server,
OrganizationId = orgUser.OrganizationId,
UserId = orgUser.UserId,
OrganizationUserId = orgUser.Id,

View File

@ -20,7 +20,6 @@ using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.Mail;
using Bit.Core.Models.StaticStore;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -200,221 +199,6 @@ public class OrganizationServiceTests
referenceEvent.Users == expectedNewUsersCount));
}
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.Plan = planType;
var plan = StaticStore.GetPlan(signup.Plan);
signup.AdditionalSeats = 0;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
signup.IsFromSecretsManagerTrial = false;
var purchaseOrganizationPlan = StaticStore.GetPlan(signup.Plan);
var result = await sutProvider.Sut.SignUpAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(
Arg.Is<Organization>(o =>
o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats
&& o.SmSeats == null
&& o.SmServiceAccounts == null));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
referenceEvent.Type == ReferenceEventType.Signup &&
referenceEvent.PlanName == plan.Name &&
referenceEvent.PlanType == plan.Type &&
referenceEvent.Seats == result.Item1.Seats &&
referenceEvent.Storage == result.Item1.MaxStorageGb));
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
Assert.NotNull(result.Item1);
Assert.NotNull(result.Item2);
await sutProvider.GetDependency<IPaymentService>().Received(1).PurchaseOrganizationAsync(
Arg.Any<Organization>(),
signup.PaymentMethodType.Value,
signup.PaymentToken,
plan,
signup.AdditionalStorageGb,
signup.AdditionalSeats,
signup.PremiumAccessAddon,
signup.TaxInfo,
false,
signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(),
signup.UseSecretsManager
);
}
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task SignUp_AssignsOwnerToDefaultCollection
(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.Plan = planType;
signup.AdditionalSeats = 0;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.UseSecretsManager = false;
// Extract orgUserId when created
Guid? orgUserId = null;
await sutProvider.GetDependency<IOrganizationUserRepository>()
.CreateAsync(Arg.Do<OrganizationUser>(ou => orgUserId = ou.Id));
var result = await sutProvider.Sut.SignUpAsync(signup);
// Assert: created a Can Manage association for the default collection
Assert.NotNull(orgUserId);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).CreateAsync(
Arg.Any<Collection>(),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cas => cas == null),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cas =>
cas.Count() == 1 &&
cas.All(c =>
c.Id == orgUserId &&
!c.ReadOnly &&
!c.HidePasswords &&
c.Manage)));
Assert.NotNull(result.Item1);
Assert.NotNull(result.Item2);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsMonthly)]
public async Task SignUp_SM_Passes(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.Plan = planType;
var plan = StaticStore.GetPlan(signup.Plan);
signup.UseSecretsManager = true;
signup.AdditionalSeats = 15;
signup.AdditionalSmSeats = 10;
signup.AdditionalServiceAccounts = 20;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.IsFromSecretsManagerTrial = false;
var result = await sutProvider.Sut.SignUpAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(
Arg.Is<Organization>(o =>
o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats
&& o.SmSeats == plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats
&& o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + signup.AdditionalServiceAccounts));
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(
Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
referenceEvent.Type == ReferenceEventType.Signup &&
referenceEvent.PlanName == plan.Name &&
referenceEvent.PlanType == plan.Type &&
referenceEvent.Seats == result.Item1.Seats &&
referenceEvent.Storage == result.Item1.MaxStorageGb));
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
Assert.NotNull(result.Item1);
Assert.NotNull(result.Item2);
await sutProvider.GetDependency<IPaymentService>().Received(1).PurchaseOrganizationAsync(
Arg.Any<Organization>(),
signup.PaymentMethodType.Value,
signup.PaymentToken,
Arg.Is<Plan>(plan),
signup.AdditionalStorageGb,
signup.AdditionalSeats,
signup.PremiumAccessAddon,
signup.TaxInfo,
false,
signup.AdditionalSmSeats.GetValueOrDefault(),
signup.AdditionalServiceAccounts.GetValueOrDefault(),
signup.IsFromSecretsManagerTrial
);
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task SignUp_SM_Throws_WhenManagedByMSP(PlanType planType, OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.Plan = planType;
signup.UseSecretsManager = true;
signup.AdditionalSeats = 15;
signup.AdditionalSmSeats = 10;
signup.AdditionalServiceAccounts = 20;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.IsFromProvider = true;
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SignUpAsync(signup));
Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_SecretManager_AdditionalServiceAccounts_NotAllowedByPlan_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.AdditionalSmSeats = 0;
signup.AdditionalSeats = 0;
signup.Plan = PlanType.Free;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
signup.AdditionalStorageGb = 0;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup));
Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_SMSeatsGreatThanPMSeat_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.AdditionalSmSeats = 100;
signup.AdditionalSeats = 10;
signup.Plan = PlanType.EnterpriseAnnually;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup));
Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SignUpAsync_InvalidateServiceAccount_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.AdditionalSmSeats = 10;
signup.AdditionalSeats = 10;
signup.Plan = PlanType.EnterpriseAnnually;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = -10;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup));
Assert.Contains("You can't subtract Machine Accounts!", exception.Message);
}
[Theory, BitAutoData]
public async Task SignupClientAsync_Succeeds(
OrganizationSignup signup,
@ -1833,11 +1617,14 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
.Returns(true);
var user = new User();
user.Email = "test@bitwarden.com";
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
Assert.Contains("you cannot restore this user because they are a member of " +
"another organization which forbids it", exception.Message.ToLowerInvariant());
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
await eventService.DidNotReceiveWithAnyArgs()
@ -1865,11 +1652,14 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
var user = new User();
user.Email = "test@bitwarden.com";
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
Assert.Contains("you cannot restore this user until they enable " +
"two-step login on their user account.", exception.Message.ToLowerInvariant());
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
await eventService.DidNotReceiveWithAnyArgs()
@ -1924,11 +1714,14 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
});
var user = new User();
user.Email = "test@bitwarden.com";
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
Assert.Contains("you cannot restore this user until " +
"they leave or remove all other organizations.", exception.Message.ToLowerInvariant());
Assert.Contains("test@bitwarden.com is not compliant with the single organization policy", exception.Message.ToLowerInvariant());
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
await eventService.DidNotReceiveWithAnyArgs()
@ -1958,11 +1751,57 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
.Returns(true);
var user = new User();
user.Email = "test@bitwarden.com";
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
Assert.Contains("you cannot restore this user because they are a member of " +
"another organization which forbids it", exception.Message.ToLowerInvariant());
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
await eventService.DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
}
[Theory, BitAutoData]
public async Task RestoreUser_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
SutProvider<OrganizationService> sutProvider)
{
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
secondOrganizationUser.UserId = organizationUser.UserId;
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var eventService = sutProvider.GetDependency<IEventService>();
organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value).Returns(new[] { organizationUser, secondOrganizationUser });
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
.Returns(new[]
{
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
});
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
.Returns(new[]
{
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
});
var user = new User();
user.Email = "test@bitwarden.com";
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login polciy", exception.Message.ToLowerInvariant());
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
await eventService.DidNotReceiveWithAnyArgs()
@ -1986,11 +1825,14 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
var user = new User();
user.Email = "test@bitwarden.com";
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
Assert.Contains("you cannot restore this user until they enable " +
"two-step login on their user account.", exception.Message.ToLowerInvariant());
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
await organizationUserRepository.DidNotReceiveWithAnyArgs().RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
await eventService.DidNotReceiveWithAnyArgs()

View File

@ -1,25 +1,13 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using AdminConsoleFixtures = Bit.Core.Test.AdminConsole.AutoFixture;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.AdminConsole.Services;
@ -27,667 +15,6 @@ namespace Bit.Core.Test.AdminConsole.Services;
[SutProviderCustomize]
public class PolicyServiceTests
{
[Theory, BitAutoData]
public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.DisableSend)] Policy policy, SutProvider<PolicyService> sutProvider)
{
SetupOrg(sutProvider, policy.OrganizationId, null);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Organization not found", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.DisableSend)] Policy policy, SutProvider<PolicyService> sutProvider)
{
var orgId = Guid.NewGuid();
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
UsePolicies = false,
});
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("cannot use policies", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_SingleOrg_RequireSsoEnabled_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.RequireSso)
.Returns(Task.FromResult(new Policy { Enabled = true }));
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Single Sign-On Authentication policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_SingleOrg_VaultTimeoutEnabled_ThrowsBadRequest([AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.MaximumVaultTimeout)
.Returns(new Policy { Enabled = true });
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Maximum Vault Timeout policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory]
[BitAutoData(PolicyType.SingleOrg)]
[BitAutoData(PolicyType.RequireSso)]
public async Task SaveAsync_PolicyRequiredByKeyConnector_DisablePolicy_ThrowsBadRequest(
PolicyType policyType,
Policy policy,
SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
policy.Type = policyType;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
var ssoConfig = new SsoConfig { Enabled = true };
var data = new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector };
ssoConfig.SetData(data);
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policy.OrganizationId)
.Returns(ssoConfig);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Key Connector is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, BitAutoData]
public async Task SaveAsync_RequireSsoPolicy_NotEnabled_ThrowsBadRequestAsync(
[AdminConsoleFixtures.Policy(PolicyType.RequireSso)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = true;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg)
.Returns(Task.FromResult(new Policy { Enabled = false }));
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_NewPolicy_Created(
[AdminConsoleFixtures.Policy(PolicyType.ResetPassword)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Id = default;
policy.Data = null;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg)
.Returns(Task.FromResult(new Policy { Enabled = true }));
var utcNow = DateTime.UtcNow;
await sutProvider.Sut.SaveAsync(policy, Guid.NewGuid());
await sutProvider.GetDependency<IEventService>().Received()
.LogPolicyEventAsync(policy, EventType.Policy_Updated);
await sutProvider.GetDependency<IPolicyRepository>().Received()
.UpsertAsync(policy);
Assert.True(policy.CreationDate - utcNow < TimeSpan.FromSeconds(1));
Assert.True(policy.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
}
[Theory, BitAutoData]
public async Task SaveAsync_VaultTimeoutPolicy_NotEnabled_ThrowsBadRequestAsync(
[AdminConsoleFixtures.Policy(PolicyType.MaximumVaultTimeout)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = true;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg)
.Returns(Task.FromResult(new Policy { Enabled = false }));
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_ExistingPolicy_UpdateTwoFactor(
Organization organization,
[AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
// If the policy that this is updating isn't enabled then do some work now that the current one is enabled
organization.UsePolicies = true;
policy.OrganizationId = organization.Id;
SetupOrg(sutProvider, organization.Id, organization);
sutProvider.GetDependency<IPolicyRepository>()
.GetByIdAsync(policy.Id)
.Returns(new Policy
{
Id = policy.Id,
Type = PolicyType.TwoFactorAuthentication,
Enabled = false
});
var orgUserDetailUserInvited = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Invited,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user1@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
var orgUserDetailUserAcceptedWith2FA = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Accepted,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user2@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var orgUserDetailUserAcceptedWithout2FA = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Accepted,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var orgUserDetailAdmin = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Admin,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "admin@test.com",
Name = "ADMIN",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policy.OrganizationId)
.Returns(new List<OrganizationUserUserDetails>
{
orgUserDetailUserInvited,
orgUserDetailUserAcceptedWith2FA,
orgUserDetailUserAcceptedWithout2FA,
orgUserDetailAdmin
});
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(orgUserDetailUserInvited, false),
(orgUserDetailUserAcceptedWith2FA, true),
(orgUserDetailUserAcceptedWithout2FA, false),
(orgUserDetailAdmin, false),
});
var removeOrganizationUserCommand = sutProvider.GetDependency<IRemoveOrganizationUserCommand>();
var utcNow = DateTime.UtcNow;
var savingUserId = Guid.NewGuid();
await sutProvider.Sut.SaveAsync(policy, savingUserId);
await removeOrganizationUserCommand.Received()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().Received()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email);
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email);
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email);
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email);
await sutProvider.GetDependency<IEventService>().Received()
.LogPolicyEventAsync(policy, EventType.Policy_Updated);
await sutProvider.GetDependency<IPolicyRepository>().Received()
.UpsertAsync(policy);
Assert.True(policy.CreationDate - utcNow < TimeSpan.FromSeconds(1));
Assert.True(policy.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
}
[Theory, BitAutoData]
public async Task SaveAsync_EnableTwoFactor_WithoutMasterPasswordOr2FA_ThrowsBadRequest(
Organization organization,
[AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
organization.UsePolicies = true;
policy.OrganizationId = organization.Id;
SetupOrg(sutProvider, organization.Id, organization);
var orgUserDetailUserWith2FAAndMP = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user1@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
var orgUserDetailUserWith2FANoMP = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user2@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
var orgUserDetailUserWithout2FA = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "user3@test.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
var orgUserDetailAdmin = new OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Admin,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "admin@test.com",
Name = "ADMIN",
UserId = Guid.NewGuid(),
HasMasterPassword = false
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policy.OrganizationId)
.Returns(new List<OrganizationUserUserDetails>
{
orgUserDetailUserWith2FAAndMP,
orgUserDetailUserWith2FANoMP,
orgUserDetailUserWithout2FA,
orgUserDetailAdmin
});
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids =>
ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value)
&& ids.Contains(orgUserDetailUserWithout2FA.UserId.Value)
&& ids.Contains(orgUserDetailAdmin.UserId.Value)))
.Returns(new List<(Guid userId, bool hasTwoFactor)>()
{
(orgUserDetailUserWith2FANoMP.UserId.Value, true),
(orgUserDetailUserWithout2FA.UserId.Value, false),
(orgUserDetailAdmin.UserId.Value, false),
});
var removeOrganizationUserCommand = sutProvider.GetDependency<IRemoveOrganizationUserCommand>();
var savingUserId = Guid.NewGuid();
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy, savingUserId));
Assert.Contains("Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await removeOrganizationUserCommand.DidNotReceiveWithAnyArgs()
.RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default);
await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(default, default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default);
await sutProvider.GetDependency<IPolicyRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, BitAutoData]
public async Task SaveAsync_ExistingPolicy_UpdateSingleOrg(
[AdminConsoleFixtures.Policy(PolicyType.TwoFactorAuthentication)] Policy policy, SutProvider<PolicyService> sutProvider)
{
// If the policy that this is updating isn't enabled then do some work now that the current one is enabled
var org = new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
Name = "TEST",
};
SetupOrg(sutProvider, policy.OrganizationId, org);
sutProvider.GetDependency<IPolicyRepository>()
.GetByIdAsync(policy.Id)
.Returns(new Policy
{
Id = policy.Id,
Type = PolicyType.SingleOrg,
Enabled = false,
});
var orgUserDetail = new Core.Models.Data.Organizations.OrganizationUsers.OrganizationUserUserDetails
{
Id = Guid.NewGuid(),
Status = OrganizationUserStatusType.Accepted,
Type = OrganizationUserType.User,
// Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync
Email = "test@bitwarden.com",
Name = "TEST",
UserId = Guid.NewGuid(),
HasMasterPassword = true
};
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyDetailsByOrganizationAsync(policy.OrganizationId)
.Returns(new List<Core.Models.Data.Organizations.OrganizationUsers.OrganizationUserUserDetails>
{
orgUserDetail,
});
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUserDetail.UserId.Value)))
.Returns(new List<(Guid userId, bool hasTwoFactor)>()
{
(orgUserDetail.UserId.Value, false),
});
var utcNow = DateTime.UtcNow;
var savingUserId = Guid.NewGuid();
await sutProvider.Sut.SaveAsync(policy, savingUserId);
await sutProvider.GetDependency<IEventService>().Received()
.LogPolicyEventAsync(policy, EventType.Policy_Updated);
await sutProvider.GetDependency<IPolicyRepository>().Received()
.UpsertAsync(policy);
Assert.True(policy.CreationDate - utcNow < TimeSpan.FromSeconds(1));
Assert.True(policy.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
}
[Theory]
[BitAutoData(true, false)]
[BitAutoData(false, true)]
[BitAutoData(false, false)]
public async Task SaveAsync_ResetPasswordPolicyRequiredByTrustedDeviceEncryption_DisablePolicyOrDisableAutomaticEnrollment_ThrowsBadRequest(
bool policyEnabled,
bool autoEnrollEnabled,
[AdminConsoleFixtures.Policy(PolicyType.ResetPassword)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
policy.Enabled = policyEnabled;
policy.SetDataModel(new ResetPasswordDataModel
{
AutoEnrollEnabled = autoEnrollEnabled
});
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policy.OrganizationId)
.Returns(ssoConfig);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Trusted device encryption is on and requires this policy.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_RequireSsoPolicyRequiredByTrustedDeviceEncryption_DisablePolicy_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.RequireSso)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policy.OrganizationId)
.Returns(ssoConfig);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Trusted device encryption is on and requires this policy.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_PolicyRequiredForAccountRecovery_NotEnabled_ThrowsBadRequestAsync(
[AdminConsoleFixtures.Policy(PolicyType.ResetPassword)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = true;
policy.SetDataModel(new ResetPasswordDataModel());
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg)
.Returns(Task.FromResult(new Policy { Enabled = false }));
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_SingleOrg_AccountRecoveryEnabled_ThrowsBadRequest(
[AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.ResetPassword)
.Returns(new Policy { Enabled = true });
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Guid.NewGuid()));
Assert.Contains("Account recovery policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, BitAutoData]
public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider<PolicyService> sutProvider)
{
@ -816,32 +143,4 @@ public class PolicyServiceTests
new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true }
});
}
[Theory, BitAutoData]
public async Task SaveAsync_GivenOrganizationUsingPoliciesAndHasVerifiedDomains_WhenSingleOrgPolicyIsDisabled_ThenAnErrorShouldBeThrownOrganizationHasVerifiedDomains(
[AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, Organization org, SutProvider<PolicyService> sutProvider)
{
org.Id = policy.OrganizationId;
org.UsePolicies = true;
policy.Enabled = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(policy.OrganizationId)
.Returns(org);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(org.Id)
.Returns(true);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy, null));
Assert.Equal("The Single organization policy is required for organizations that have enabled domain verification.", badRequestException.Message);
}
}

View File

@ -1,8 +1,9 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
@ -338,16 +339,26 @@ public class SsoConfigServiceTests
await sutProvider.Sut.SaveAsync(ssoConfig, organization);
await sutProvider.GetDependency<IPolicyService>().Received(1)
await sutProvider.GetDependency<ISavePolicyCommand>().Received(1)
.SaveAsync(
Arg.Is<Policy>(t => t.Type == PolicyType.SingleOrg),
null
Arg.Is<PolicyUpdate>(t => t.Type == PolicyType.SingleOrg &&
t.OrganizationId == organization.Id &&
t.Enabled)
);
await sutProvider.GetDependency<IPolicyService>().Received(1)
await sutProvider.GetDependency<ISavePolicyCommand>().Received(1)
.SaveAsync(
Arg.Is<Policy>(t => t.Type == PolicyType.ResetPassword && t.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled),
null
Arg.Is<PolicyUpdate>(t => t.Type == PolicyType.ResetPassword &&
t.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled &&
t.OrganizationId == organization.Id &&
t.Enabled)
);
await sutProvider.GetDependency<ISavePolicyCommand>().Received(1)
.SaveAsync(
Arg.Is<PolicyUpdate>(t => t.Type == PolicyType.RequireSso &&
t.OrganizationId == organization.Id &&
t.Enabled)
);
await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs()

View File

@ -1581,21 +1581,18 @@ public class SubscriberServiceTests
#region VerifyBankAccount
[Theory, BitAutoData]
public async Task VerifyBankAccount_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider) => await Assert.ThrowsAsync<ArgumentNullException>(
() => sutProvider.Sut.VerifyBankAccount(null, (0, 0)));
[Theory, BitAutoData]
public async Task VerifyBankAccount_NoSetupIntentId_ThrowsBillingException(
Provider provider,
SutProvider<SubscriberService> sutProvider) => await ThrowsBillingExceptionAsync(() => sutProvider.Sut.VerifyBankAccount(provider, (1, 1)));
SutProvider<SubscriberService> sutProvider) => await ThrowsBillingExceptionAsync(() => sutProvider.Sut.VerifyBankAccount(provider, ""));
[Theory, BitAutoData]
public async Task VerifyBankAccount_MakesCorrectInvocations(
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
const string descriptorCode = "SM1234";
var setupIntent = new SetupIntent
{
Id = "setup_intent_id",
@ -1608,11 +1605,11 @@ public class SubscriberServiceTests
stripeAdapter.SetupIntentGet(setupIntent.Id).Returns(setupIntent);
await sutProvider.Sut.VerifyBankAccount(provider, (1, 1));
await sutProvider.Sut.VerifyBankAccount(provider, descriptorCode);
await stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id,
Arg.Is<SetupIntentVerifyMicrodepositsOptions>(
options => options.Amounts[0] == 1 && options.Amounts[1] == 1));
options => options.DescriptorCode == descriptorCode));
await stripeAdapter.Received(1).PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
Arg.Is<PaymentMethodAttachOptions>(

View File

@ -0,0 +1,197 @@
#nullable enable
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Commands;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Core.Test.KeyManagement.Commands;
[SutProviderCustomize]
public class RegenerateUserAsymmetricKeysCommandTests
{
[Theory]
[BitAutoData]
public async Task RegenerateKeysAsync_NoCurrentContext_NotFoundException(
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,
UserAsymmetricKeys userAsymmetricKeys)
{
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsNullForAnyArgs();
var usersOrganizationAccounts = new List<OrganizationUser>();
var designatedEmergencyAccess = new List<EmergencyAccessDetails>();
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,
usersOrganizationAccounts, designatedEmergencyAccess));
}
[Theory]
[BitAutoData]
public async Task RegenerateKeysAsync_UserHasNoSharedAccess_Success(
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,
UserAsymmetricKeys userAsymmetricKeys)
{
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId);
var usersOrganizationAccounts = new List<OrganizationUser>();
var designatedEmergencyAccess = new List<EmergencyAccessDetails>();
await sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,
usersOrganizationAccounts, designatedEmergencyAccess);
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()
.Received(1)
.RegenerateUserAsymmetricKeysAsync(Arg.Is(userAsymmetricKeys));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushSyncSettingsAsync(Arg.Is(userAsymmetricKeys.UserId));
}
[Theory]
[BitAutoData(false, false, true)]
[BitAutoData(false, true, false)]
[BitAutoData(false, true, true)]
[BitAutoData(true, false, false)]
[BitAutoData(true, false, true)]
[BitAutoData(true, true, false)]
[BitAutoData(true, true, true)]
public async Task RegenerateKeysAsync_UserIdMisMatch_NotFoundException(
bool userAsymmetricKeysMismatch,
bool orgMismatch,
bool emergencyAccessMismatch,
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,
UserAsymmetricKeys userAsymmetricKeys,
ICollection<OrganizationUser> usersOrganizationAccounts,
ICollection<EmergencyAccessDetails> designatedEmergencyAccess)
{
sutProvider.GetDependency<ICurrentContext>().UserId
.ReturnsForAnyArgs(userAsymmetricKeysMismatch ? new Guid() : userAsymmetricKeys.UserId);
if (!orgMismatch)
{
usersOrganizationAccounts =
SetupOrganizationUserAccounts(userAsymmetricKeys.UserId, usersOrganizationAccounts);
}
if (!emergencyAccessMismatch)
{
designatedEmergencyAccess = SetupEmergencyAccess(userAsymmetricKeys.UserId, designatedEmergencyAccess);
}
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,
usersOrganizationAccounts, designatedEmergencyAccess));
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()
.ReceivedWithAnyArgs(0)
.RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>());
await sutProvider.GetDependency<IPushNotificationService>()
.ReceivedWithAnyArgs(0)
.PushSyncSettingsAsync(Arg.Any<Guid>());
}
[Theory]
[BitAutoData(OrganizationUserStatusType.Confirmed)]
[BitAutoData(OrganizationUserStatusType.Revoked)]
public async Task RegenerateKeysAsync_UserInOrganizations_BadRequestException(
OrganizationUserStatusType organizationUserStatus,
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,
UserAsymmetricKeys userAsymmetricKeys,
ICollection<OrganizationUser> usersOrganizationAccounts)
{
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId);
usersOrganizationAccounts = CreateInOrganizationAccounts(userAsymmetricKeys.UserId, organizationUserStatus,
usersOrganizationAccounts);
var designatedEmergencyAccess = new List<EmergencyAccessDetails>();
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,
usersOrganizationAccounts, designatedEmergencyAccess));
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()
.ReceivedWithAnyArgs(0)
.RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>());
await sutProvider.GetDependency<IPushNotificationService>()
.ReceivedWithAnyArgs(0)
.PushSyncSettingsAsync(Arg.Any<Guid>());
}
[Theory]
[BitAutoData(EmergencyAccessStatusType.Confirmed)]
[BitAutoData(EmergencyAccessStatusType.RecoveryApproved)]
[BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)]
public async Task RegenerateKeysAsync_UserHasDesignatedEmergencyAccess_BadRequestException(
EmergencyAccessStatusType statusType,
SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,
UserAsymmetricKeys userAsymmetricKeys,
ICollection<EmergencyAccessDetails> designatedEmergencyAccess)
{
sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId);
designatedEmergencyAccess =
CreateDesignatedEmergencyAccess(userAsymmetricKeys.UserId, statusType, designatedEmergencyAccess);
var usersOrganizationAccounts = new List<OrganizationUser>();
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,
usersOrganizationAccounts, designatedEmergencyAccess));
await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()
.ReceivedWithAnyArgs(0)
.RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>());
await sutProvider.GetDependency<IPushNotificationService>()
.ReceivedWithAnyArgs(0)
.PushSyncSettingsAsync(Arg.Any<Guid>());
}
private static ICollection<OrganizationUser> CreateInOrganizationAccounts(Guid userId,
OrganizationUserStatusType organizationUserStatus, ICollection<OrganizationUser> organizationUserAccounts)
{
foreach (var organizationUserAccount in organizationUserAccounts)
{
organizationUserAccount.UserId = userId;
organizationUserAccount.Status = organizationUserStatus;
}
return organizationUserAccounts;
}
private static ICollection<EmergencyAccessDetails> CreateDesignatedEmergencyAccess(Guid userId,
EmergencyAccessStatusType status, ICollection<EmergencyAccessDetails> designatedEmergencyAccess)
{
foreach (var designated in designatedEmergencyAccess)
{
designated.GranteeId = userId;
designated.Status = status;
}
return designatedEmergencyAccess;
}
private static ICollection<OrganizationUser> SetupOrganizationUserAccounts(Guid userId,
ICollection<OrganizationUser> organizationUserAccounts)
{
foreach (var organizationUserAccount in organizationUserAccounts)
{
organizationUserAccount.UserId = userId;
}
return organizationUserAccounts;
}
private static ICollection<EmergencyAccessDetails> SetupEmergencyAccess(Guid userId,
ICollection<EmergencyAccessDetails> emergencyAccessDetails)
{
foreach (var emergencyAccessDetail in emergencyAccessDetails)
{
emergencyAccessDetail.GranteeId = userId;
}
return emergencyAccessDetails;
}
}

View File

@ -1,8 +1,8 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.UserKey.Implementations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.UserKey;
namespace Bit.Core.Test.KeyManagement.UserFeatures.UserKey;
[SutProviderCustomize]
public class RotateUserKeyCommandTests

View File

@ -111,7 +111,8 @@ public static class OrganizationLicenseFileFixtures
SmServiceAccounts = 8,
MaxAutoscaleSmSeats = 101,
MaxAutoscaleSmServiceAccounts = 102,
LimitCollectionCreationDeletion = true,
LimitCollectionCreation = true,
LimitCollectionDeletion = true,
AllowAdminAccessToAllCollectionItems = true,
};
}

View File

@ -1,4 +1,5 @@
using System.Text.Json;
using System.Security.Claims;
using System.Text.Json;
using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -36,7 +37,7 @@ public class OrganizationLicenseTests
[Theory]
[BitAutoData(OrganizationLicense.CurrentLicenseFileVersion)] // Previous version (this property is 1 behind)
[BitAutoData(OrganizationLicense.CurrentLicenseFileVersion + 1)] // Current version
public void OrganizationLicense_LoadedFromDisk_VerifyData_Passes(int licenseVersion)
public void OrganizationLicense_LoadedFromDisk_VerifyData_Passes(int licenseVersion, ClaimsPrincipal claimsPrincipal)
{
var license = OrganizationLicenseFileFixtures.GetVersion(licenseVersion);
@ -49,7 +50,7 @@ public class OrganizationLicenseTests
{
Id = new Guid(OrganizationLicenseFileFixtures.InstallationId)
});
Assert.True(license.VerifyData(organization, globalSettings));
Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings));
}
/// <summary>

View File

@ -9,9 +9,32 @@ public class NotificationStatusDetailsCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<NotificationStatusDetails>(composer => composer.With(n => n.Id, Guid.NewGuid())
.With(n => n.UserId, Guid.NewGuid())
.With(n => n.OrganizationId, Guid.NewGuid()));
fixture.Customize<NotificationStatusDetails>(composer =>
{
return composer.With(n => n.Id, Guid.NewGuid())
.With(n => n.UserId, Guid.NewGuid())
.With(n => n.OrganizationId, Guid.NewGuid());
});
}
}
public class NotificationStatusDetailsListCustomization(int count) : ICustomization
{
public void Customize(IFixture fixture)
{
var customization = new NotificationStatusDetailsCustomization();
fixture.Customize<IEnumerable<NotificationStatusDetails>>(composer => composer.FromFactory(() =>
{
var notifications = new List<NotificationStatusDetails>();
for (var i = 0; i < count; i++)
{
customization.Customize(fixture);
var notificationStatusDetails = fixture.Create<NotificationStatusDetails>();
notifications.Add(notificationStatusDetails);
}
return notifications;
}));
}
}
@ -19,3 +42,8 @@ public class NotificationStatusDetailsCustomizeAttribute : BitCustomizeAttribute
{
public override ICustomization GetCustomization() => new NotificationStatusDetailsCustomization();
}
public class NotificationStatusDetailsListCustomizeAttribute(int count) : BitCustomizeAttribute
{
public override ICustomization GetCustomization() => new NotificationStatusDetailsListCustomization(count);
}

View File

@ -2,6 +2,7 @@
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.NotificationCenter.Models.Data;
using Bit.Core.NotificationCenter.Models.Filter;
using Bit.Core.NotificationCenter.Queries;
@ -19,37 +20,49 @@ namespace Bit.Core.Test.NotificationCenter.Queries;
public class GetNotificationStatusDetailsForUserQueryTest
{
private static void Setup(SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter statusFilter, Guid? userId)
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter statusFilter, Guid? userId,
PageOptions pageOptions, string? continuationToken)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<INotificationRepository>().GetByUserIdAndStatusAsync(
userId.GetValueOrDefault(Guid.NewGuid()), Arg.Any<ClientType>(), statusFilter)
.Returns(notificationsStatusDetails);
sutProvider.GetDependency<INotificationRepository>()
.GetByUserIdAndStatusAsync(userId.GetValueOrDefault(Guid.NewGuid()), Arg.Any<ClientType>(), statusFilter,
pageOptions)
.Returns(new PagedResult<NotificationStatusDetails>
{
Data = notificationsStatusDetails,
ContinuationToken = continuationToken
});
}
[Theory]
[BitAutoData]
public async Task GetByUserIdStatusFilterAsync_NotLoggedIn_NotFoundException(
SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter)
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter,
PageOptions pageOptions, string? continuationToken)
{
Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, userId: null);
Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, userId: null, pageOptions,
continuationToken);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter));
sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter, pageOptions));
}
[Theory]
[BitAutoData]
public async Task GetByUserIdStatusFilterAsync_NotificationsFound_Returned(
SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter)
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter,
PageOptions pageOptions, string? continuationToken)
{
Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, Guid.NewGuid());
Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, Guid.NewGuid(), pageOptions,
continuationToken);
var actualNotificationsStatusDetails =
await sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter);
var actualNotificationsStatusDetailsPagedResult =
await sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter, pageOptions);
Assert.Equal(notificationsStatusDetails, actualNotificationsStatusDetails);
Assert.NotNull(actualNotificationsStatusDetailsPagedResult);
Assert.Equal(notificationsStatusDetails, actualNotificationsStatusDetailsPagedResult.Data);
Assert.Equal(continuationToken, actualNotificationsStatusDetailsPagedResult.ContinuationToken);
}
}

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -11,6 +13,7 @@ using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses;
@ -62,4 +65,34 @@ public class CloudGetOrganizationLicenseQueryTests
Assert.Equal(installationId, result.InstallationId);
Assert.Equal(licenseSignature, result.SignatureBytes);
}
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_MSPManagedOrganization_UsesProviderSubscription(SutProvider<CloudGetOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo,
byte[] licenseSignature, Provider provider)
{
organization.Status = OrganizationStatusType.Managed;
organization.ExpirationDate = null;
subInfo.Subscription = new SubscriptionInfo.BillingSubscription(new Subscription
{
CurrentPeriodStart = DateTime.UtcNow,
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)
});
installation.Enabled = true;
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider);
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(provider).Returns(subInfo);
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);
Assert.Equal(LicenseType.Organization, result.LicenseType);
Assert.Equal(organization.Id, result.Id);
Assert.Equal(installationId, result.InstallationId);
Assert.Equal(licenseSignature, result.SignatureBytes);
Assert.Equal(DateTime.UtcNow.AddYears(1).Date, result.Expires!.Value.Date);
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using System.Security.Claims;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
@ -48,6 +49,9 @@ public class UpdateOrganizationLicenseCommandTests
license.InstallationId = globalSettings.Installation.Id;
license.LicenseType = LicenseType.Organization;
sutProvider.GetDependency<ILicensingService>().VerifyLicense(license).Returns(true);
sutProvider.GetDependency<ILicensingService>()
.GetClaimsPrincipalFromLicense(license)
.Returns((ClaimsPrincipal)null);
// Passing values for SelfHostedOrganizationDetails.CanUseLicense
// NSubstitute cannot override non-virtual members so we have to ensure the real method passes
@ -80,7 +84,9 @@ public class UpdateOrganizationLicenseCommandTests
.ReplaceAndUpdateCacheAsync(Arg.Is<Organization>(
org => AssertPropertyEqual(license, org,
"Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType",
"Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", "ExpirationWithoutGracePeriod") &&
"Hash", "Signature", "SignatureBytes", "InstallationId", "Expires",
"ExpirationWithoutGracePeriod", "Token", "LimitCollectionCreationDeletion",
"LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems") &&
// Same property but different name, use explicit mapping
org.ExpirationDate == license.Expires));
}

View File

@ -1,6 +1,9 @@
using System.Text.Json;
using System.Security.Claims;
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
@ -9,13 +12,17 @@ using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -62,6 +69,9 @@ public class UserServiceTests
sutProvider.GetDependency<ILicensingService>()
.VerifyLicense(userLicense)
.Returns(true);
sutProvider.GetDependency<ILicensingService>()
.GetClaimsPrincipalFromLicense(userLicense)
.Returns((ClaimsPrincipal)null);
await sutProvider.Sut.UpdateLicenseAsync(user, userLicense);
@ -231,41 +241,7 @@ public class UserServiceTests
});
// HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured
var sut = new UserService(
sutProvider.GetDependency<IUserRepository>(),
sutProvider.GetDependency<ICipherRepository>(),
sutProvider.GetDependency<IOrganizationUserRepository>(),
sutProvider.GetDependency<IOrganizationRepository>(),
sutProvider.GetDependency<IMailService>(),
sutProvider.GetDependency<IPushNotificationService>(),
sutProvider.GetDependency<IUserStore<User>>(),
sutProvider.GetDependency<IOptions<IdentityOptions>>(),
sutProvider.GetDependency<IPasswordHasher<User>>(),
sutProvider.GetDependency<IEnumerable<IUserValidator<User>>>(),
sutProvider.GetDependency<IEnumerable<IPasswordValidator<User>>>(),
sutProvider.GetDependency<ILookupNormalizer>(),
sutProvider.GetDependency<IdentityErrorDescriber>(),
sutProvider.GetDependency<IServiceProvider>(),
sutProvider.GetDependency<ILogger<UserManager<User>>>(),
sutProvider.GetDependency<ILicensingService>(),
sutProvider.GetDependency<IEventService>(),
sutProvider.GetDependency<IApplicationCacheService>(),
sutProvider.GetDependency<IDataProtectionProvider>(),
sutProvider.GetDependency<IPaymentService>(),
sutProvider.GetDependency<IPolicyRepository>(),
sutProvider.GetDependency<IPolicyService>(),
sutProvider.GetDependency<IReferenceEventService>(),
sutProvider.GetDependency<IFido2>(),
sutProvider.GetDependency<ICurrentContext>(),
sutProvider.GetDependency<IGlobalSettings>(),
sutProvider.GetDependency<IAcceptOrgUserCommand>(),
sutProvider.GetDependency<IProviderUserRepository>(),
sutProvider.GetDependency<IStripeSyncService>(),
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
sutProvider.GetDependency<IFeatureService>(),
sutProvider.GetDependency<IPremiumUserBillingService>(),
sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
);
var sut = RebuildSut(sutProvider);
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
@ -349,6 +325,262 @@ public class UserServiceTests
Assert.False(result);
}
[Theory, BitAutoData]
public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RemovesUserFromOrganizationAndSendsEmail(
SutProvider<UserService> sutProvider, User user, Organization organization)
{
// Arrange
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new() { Enabled = true }
});
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
.Returns(
[
new OrganizationUserPolicyDetails
{
OrganizationId = organization.Id,
PolicyType = PolicyType.TwoFactorAuthentication,
PolicyEnabled = true
}
]);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);
// Act
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
// Assert
await sutProvider.GetDependency<IUserRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
.Received(1)
.RemoveUserAsync(organization.Id, user.Id);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), user.Email);
}
[Theory, BitAutoData]
public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_UserHasOneProviderEnabled_DoesNotRemoveUserFromOrganization(
SutProvider<UserService> sutProvider, User user, Organization organization)
{
// Arrange
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new() { Enabled = true },
[TwoFactorProviderType.Remember] = new() { Enabled = true }
});
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
.Returns(
[
new OrganizationUserPolicyDetails
{
OrganizationId = organization.Id,
PolicyType = PolicyType.TwoFactorAuthentication,
PolicyEnabled = true
}
]);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Remember] = new() { Enabled = true }
}, JsonHelpers.LegacyEnumKeyResolver);
// Act
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
// Assert
await sutProvider.GetDependency<IUserRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
.DidNotReceiveWithAnyArgs()
.RemoveUserAsync(default, default);
await sutProvider.GetDependency<IMailService>()
.DidNotReceiveWithAnyArgs()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(default, default);
}
[Theory, BitAutoData]
public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_WhenOrganizationHas2FAPolicyEnabled_WhenUserIsManaged_DisablingAllProviders_RemovesOrRevokesUserAndSendsEmail(
SutProvider<UserService> sutProvider, User user, Organization organization1, Organization organization2)
{
// Arrange
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new() { Enabled = true }
});
organization1.Enabled = organization2.Enabled = true;
organization1.UseSso = organization2.UseSso = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
.Returns(
[
new OrganizationUserPolicyDetails
{
OrganizationId = organization1.Id,
PolicyType = PolicyType.TwoFactorAuthentication,
PolicyEnabled = true
},
new OrganizationUserPolicyDetails
{
OrganizationId = organization2.Id,
PolicyType = PolicyType.TwoFactorAuthentication,
PolicyEnabled = true
}
]);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization1.Id)
.Returns(organization1);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization2.Id)
.Returns(organization2);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByVerifiedUserEmailDomainAsync(user.Id)
.Returns(new[] { organization1 });
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);
// Act
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
// Assert
await sutProvider.GetDependency<IUserRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
// Revoke the user from the first organization because they are managed by it
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(
Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization1.Id &&
r.OrganizationUsers.First().UserId == user.Id &&
r.OrganizationUsers.First().OrganizationId == organization1.Id));
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization1.DisplayName(), user.Email);
// Remove the user from the second organization because they are not managed by it
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
.Received(1)
.RemoveUserAsync(organization2.Id, user.Id);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization2.DisplayName(), user.Email);
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_UserNull_SendOTPAsyncNotCalled(
SutProvider<UserService> sutProvider, string email, string secret)
{
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.Returns(null as User);
await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOTPEmailAsync(Arg.Any<string>(), Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_SecretNotValid_SendOTPAsyncNotCalled(
SutProvider<UserService> sutProvider, string email, string secret)
{
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.Returns(null as User);
await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOTPEmailAsync(Arg.Any<string>(), Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_SendsToken_Success(
SutProvider<UserService> sutProvider, User user)
{
// Arrange
var testPassword = "test_password";
var tokenProvider = SetupFakeTokenProvider(sutProvider, user);
SetupUserAndDevice(user, true);
// Setup the fake password verification
var substitutedUserPasswordStore = Substitute.For<IUserPasswordStore<User>>();
substitutedUserPasswordStore
.GetPasswordHashAsync(user, Arg.Any<CancellationToken>())
.Returns((ci) =>
{
return Task.FromResult("hashed_test_password");
});
sutProvider.SetDependency<IUserStore<User>>(substitutedUserPasswordStore, "store");
sutProvider.GetDependency<IPasswordHasher<User>>("passwordHasher")
.VerifyHashedPassword(user, "hashed_test_password", testPassword)
.Returns((ci) =>
{
return PasswordVerificationResult.Success;
});
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(user.Email)
.Returns(user);
// HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured
var sut = RebuildSut(sutProvider);
await sut.ResendNewDeviceVerificationEmail(user.Email, testPassword);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOTPEmailAsync(user.Email, Arg.Any<string>());
}
[Theory]
[BitAutoData("")]
[BitAutoData("null")]
public async Task SendOTPAsync_UserEmailNull_ThrowsBadRequest(
string email,
SutProvider<UserService> sutProvider, User user)
{
user.Email = email == "null" ? null : "";
var expectedMessage = "No user email.";
try
{
await sutProvider.Sut.SendOTPAsync(user);
}
catch (BadRequestException ex)
{
Assert.Equal(ex.Message, expectedMessage);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOTPEmailAsync(Arg.Any<string>(), Arg.Any<string>());
}
}
private static void SetupUserAndDevice(User user,
bool shouldHavePassword)
{
@ -400,4 +632,44 @@ public class UserServiceTests
return fakeUserTwoFactorProvider;
}
private IUserService RebuildSut(SutProvider<UserService> sutProvider)
{
return new UserService(
sutProvider.GetDependency<IUserRepository>(),
sutProvider.GetDependency<ICipherRepository>(),
sutProvider.GetDependency<IOrganizationUserRepository>(),
sutProvider.GetDependency<IOrganizationRepository>(),
sutProvider.GetDependency<IMailService>(),
sutProvider.GetDependency<IPushNotificationService>(),
sutProvider.GetDependency<IUserStore<User>>(),
sutProvider.GetDependency<IOptions<IdentityOptions>>(),
sutProvider.GetDependency<IPasswordHasher<User>>(),
sutProvider.GetDependency<IEnumerable<IUserValidator<User>>>(),
sutProvider.GetDependency<IEnumerable<IPasswordValidator<User>>>(),
sutProvider.GetDependency<ILookupNormalizer>(),
sutProvider.GetDependency<IdentityErrorDescriber>(),
sutProvider.GetDependency<IServiceProvider>(),
sutProvider.GetDependency<ILogger<UserManager<User>>>(),
sutProvider.GetDependency<ILicensingService>(),
sutProvider.GetDependency<IEventService>(),
sutProvider.GetDependency<IApplicationCacheService>(),
sutProvider.GetDependency<IDataProtectionProvider>(),
sutProvider.GetDependency<IPaymentService>(),
sutProvider.GetDependency<IPolicyRepository>(),
sutProvider.GetDependency<IPolicyService>(),
sutProvider.GetDependency<IReferenceEventService>(),
sutProvider.GetDependency<IFido2>(),
sutProvider.GetDependency<ICurrentContext>(),
sutProvider.GetDependency<IGlobalSettings>(),
sutProvider.GetDependency<IAcceptOrgUserCommand>(),
sutProvider.GetDependency<IProviderUserRepository>(),
sutProvider.GetDependency<IStripeSyncService>(),
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
sutProvider.GetDependency<IFeatureService>(),
sutProvider.GetDependency<IPremiumUserBillingService>(),
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
);
}
}

View File

@ -4,8 +4,8 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.ReportFeatures;
using Bit.Core.Tools.ReportFeatures.Requests;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Requests;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;

View File

@ -0,0 +1,104 @@
using AutoFixture;
using Bit.Core.Exceptions;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.ReportFeatures;
using Bit.Core.Tools.ReportFeatures.Requests;
using Bit.Core.Tools.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Tools.ReportFeatures;
[SutProviderCustomize]
public class DeletePasswordHealthReportApplicationCommandTests
{
[Theory, BitAutoData]
public async Task DropPasswordHealthReportApplicationAsync_withValidRequest_Success(
SutProvider<DropPasswordHealthReportApplicationCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var passwordHealthReportApplications = fixture.CreateMany<PasswordHealthReportApplication>(2).ToList();
// only take one id from the list - we only want to drop one record
var request = fixture.Build<DropPasswordHealthReportApplicationRequest>()
.With(x => x.PasswordHealthReportApplicationIds,
passwordHealthReportApplications.Select(x => x.Id).Take(1).ToList())
.Create();
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(passwordHealthReportApplications);
// Act
await sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request);
// Assert
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(1)
.DeleteAsync(Arg.Is<PasswordHealthReportApplication>(_ =>
request.PasswordHealthReportApplicationIds.Contains(_.Id)));
}
[Theory, BitAutoData]
public async Task DropPasswordHealthReportApplicationAsync_withValidRequest_nothingToDrop(
SutProvider<DropPasswordHealthReportApplicationCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
var passwordHealthReportApplications = fixture.CreateMany<PasswordHealthReportApplication>(2).ToList();
// we are passing invalid data
var request = fixture.Build<DropPasswordHealthReportApplicationRequest>()
.With(x => x.PasswordHealthReportApplicationIds, new List<Guid> { Guid.NewGuid() })
.Create();
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(passwordHealthReportApplications);
// Act
await sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request);
// Assert
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(0)
.DeleteAsync(Arg.Any<PasswordHealthReportApplication>());
}
[Theory, BitAutoData]
public async Task DropPasswordHealthReportApplicationAsync_withNodata_fails(
SutProvider<DropPasswordHealthReportApplicationCommand> sutProvider)
{
// Arrange
var fixture = new Fixture();
// we are passing invalid data
var request = fixture.Build<DropPasswordHealthReportApplicationRequest>()
.Create();
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.GetByOrganizationIdAsync(Arg.Any<Guid>())
.Returns(null as List<PasswordHealthReportApplication>);
// Act
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request));
// Assert
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(1)
.GetByOrganizationIdAsync(request.OrganizationId);
await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
.Received(0)
.DeleteAsync(Arg.Any<PasswordHealthReportApplication>());
}
}

View File

@ -0,0 +1,25 @@
using AutoFixture;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
namespace Bit.Core.Test.Vault.AutoFixture;
public class SecurityTaskFixtures : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<SecurityTask>(composer =>
composer
.With(task => task.Id, Guid.NewGuid())
.With(task => task.OrganizationId, Guid.NewGuid())
.With(task => task.Status, SecurityTaskStatus.Pending)
.Without(x => x.CipherId)
);
}
}
public class SecurityTaskCustomizeAttribute : BitCustomizeAttribute
{
public override ICustomization GetCustomization() => new SecurityTaskFixtures();
}

View File

@ -0,0 +1,83 @@
#nullable enable
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Test.Vault.AutoFixture;
using Bit.Core.Vault.Authorization;
using Bit.Core.Vault.Commands;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Vault.Commands;
[SutProviderCustomize]
[SecurityTaskCustomize]
public class MarkTaskAsCompletedCommandTest
{
private static void Setup(SutProvider<MarkTaskAsCompletedCommand> sutProvider, Guid taskId, SecurityTask? securityTask, Guid? userId, bool authorizedUpdate = false)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ISecurityTaskRepository>()
.GetByIdAsync(taskId)
.Returns(securityTask);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), securityTask ?? Arg.Any<SecurityTask>(),
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>
reqs.Contains(SecurityTaskOperations.Update)))
.Returns(authorizedUpdate ? AuthorizationResult.Success() : AuthorizationResult.Failed());
}
[Theory]
[BitAutoData]
public async Task CompleteAsync_NotLoggedIn_NotFoundException(
SutProvider<MarkTaskAsCompletedCommand> sutProvider,
Guid taskId,
SecurityTask securityTask)
{
Setup(sutProvider, taskId, securityTask, null, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CompleteAsync(taskId));
}
[Theory]
[BitAutoData]
public async Task CompleteAsync_TaskNotFound_NotFoundException(
SutProvider<MarkTaskAsCompletedCommand> sutProvider,
Guid taskId)
{
Setup(sutProvider, taskId, null, Guid.NewGuid(), true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CompleteAsync(taskId));
}
[Theory]
[BitAutoData]
public async Task CompleteAsync_AuthorizationFailed_NotFoundException(
SutProvider<MarkTaskAsCompletedCommand> sutProvider,
Guid taskId,
SecurityTask securityTask)
{
Setup(sutProvider, taskId, securityTask, Guid.NewGuid());
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CompleteAsync(taskId));
}
[Theory]
[BitAutoData]
public async Task CompleteAsync_Success(
SutProvider<MarkTaskAsCompletedCommand> sutProvider,
Guid taskId,
SecurityTask securityTask)
{
Setup(sutProvider, taskId, securityTask, Guid.NewGuid(), true);
await sutProvider.Sut.CompleteAsync(taskId);
await sutProvider.GetDependency<ISecurityTaskRepository>().Received(1).ReplaceAsync(securityTask);
}
}

View File

@ -0,0 +1,92 @@
using AutoFixture;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Queries;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Vault.Queries;
[SutProviderCustomize]
public class OrganizationCiphersQueryTests
{
[Theory, BitAutoData]
public async Task GetOrganizationCiphersInCollections_ReturnsFilteredCiphers(
Guid organizationId, SutProvider<OrganizationCiphersQuery> sutProvider)
{
var fixture = new Fixture();
var otherCollectionId = Guid.NewGuid();
var targetCollectionId = Guid.NewGuid();
var otherCipher = fixture.Create<CipherOrganizationDetails>();
var targetCipher = fixture.Create<CipherOrganizationDetails>();
var bothCipher = fixture.Create<CipherOrganizationDetails>();
var noCipher = fixture.Create<CipherOrganizationDetails>();
var ciphers = new List<CipherOrganizationDetails>
{
otherCipher, // not in the target collection
targetCipher, // in the target collection
bothCipher, // in both collections
noCipher // not in any collection
};
ciphers.ForEach(c =>
{
c.OrganizationId = organizationId;
c.UserId = null;
});
var otherCollectionCipher = new CollectionCipher
{
CollectionId = otherCollectionId,
CipherId = otherCipher.Id
};
var targetCollectionCipher = new CollectionCipher
{
CollectionId = targetCollectionId,
CipherId = targetCipher.Id
};
var bothCollectionCipher1 = new CollectionCipher
{
CollectionId = targetCollectionId,
CipherId = bothCipher.Id
};
var bothCollectionCipher2 = new CollectionCipher
{
CollectionId = otherCollectionId,
CipherId = bothCipher.Id
};
sutProvider.GetDependency<ICipherRepository>().GetManyOrganizationDetailsByOrganizationIdAsync(organizationId)
.Returns(ciphers);
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByOrganizationIdAsync(organizationId).Returns(
[
targetCollectionCipher,
otherCollectionCipher,
bothCollectionCipher1,
bothCollectionCipher2
]);
var result = await sutProvider
.Sut
.GetOrganizationCiphersByCollectionIds(organizationId, [targetCollectionId]);
result = result.ToList();
Assert.Equal(2, result.Count());
Assert.Contains(result, c =>
c.Id == targetCipher.Id &&
c.CollectionIds.Count() == 1 &&
c.CollectionIds.Any(cId => cId == targetCollectionId));
Assert.Contains(result, c =>
c.Id == bothCipher.Id &&
c.CollectionIds.Count() == 2 &&
c.CollectionIds.Any(cId => cId == targetCollectionId) &&
c.CollectionIds.Any(cId => cId == otherCollectionId));
}
}

View File

@ -5,14 +5,11 @@ using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Identity.IdentityServer.RequestValidators;
using Bit.Identity.Models.Request.Accounts;
using Bit.IntegrationTestCommon.Factories;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
namespace Bit.Identity.IntegrationTest.RequestValidation;
@ -217,48 +214,6 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
Assert.Equal("Username or password is incorrect. Try again.", errorMessage);
}
[Fact]
public async Task ValidateAsync_DeviceSaveAsync_ReturnsNullDevice_ErrorResult()
{
// Arrange
var factory = new IdentityApplicationFactory();
// Stub DeviceValidator
factory.SubstituteService<IDeviceValidator>(sub =>
{
sub.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(null as Device);
});
// Add User
await factory.RegisterAsync(new RegisterRequestModel
{
Email = DefaultUsername,
MasterPasswordHash = DefaultPassword
});
var userManager = factory.GetService<UserManager<User>>();
await factory.RegisterAsync(new RegisterRequestModel
{
Email = DefaultUsername,
MasterPasswordHash = DefaultPassword
});
var user = await userManager.FindByEmailAsync(DefaultUsername);
Assert.NotNull(user);
// Act
var context = await factory.Server.PostAsync("/connect/token",
GetFormUrlEncodedContent(),
context => context.SetAuthEmail(DefaultUsername));
// Assert
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = body.RootElement;
var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object);
var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString();
Assert.Equal("No device information provided.", errorMessage);
}
private async Task EnsureUserCreatedAsync(IdentityApplicationFactory factory = null)
{
factory ??= _factory;
@ -290,6 +245,18 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
});
}
private FormUrlEncodedContent GetDefaultFormUrlEncodedContentWithoutDevice()
{
return new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "scope", "api offline_access" },
{ "client_id", "web" },
{ "grant_type", "password" },
{ "username", DefaultUsername },
{ "password", DefaultPassword },
});
}
private static string DeviceTypeAsString(DeviceType deviceType)
{
return ((int)deviceType).ToString();

View File

@ -22,7 +22,6 @@ using NSubstitute;
using Xunit;
using AuthFixtures = Bit.Identity.Test.AutoFixture;
namespace Bit.Identity.Test.IdentityServer;
public class BaseRequestValidatorTests
@ -82,10 +81,10 @@ public class BaseRequestValidatorTests
}
/* Logic path
ValidateAsync -> _Logger.LogInformation
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|-> SetErrorResult
*/
* ValidateAsync -> _Logger.LogInformation
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
* |-> SetErrorResult
*/
[Theory, BitAutoData]
public async Task ValidateAsync_IsBot_UserNotNull_ShouldBuildErrorResult_ShouldLogFailedLoginEvent(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
@ -112,11 +111,11 @@ public class BaseRequestValidatorTests
}
/* Logic path
ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
(self hosted) |-> _logger.LogWarning()
|-> SetErrorResult
*/
* ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
* (self hosted) |-> _logger.LogWarning()
* |-> SetErrorResult
*/
[Theory, BitAutoData]
public async Task ValidateAsync_ContextNotValid_SelfHosted_ShouldBuildErrorResult_ShouldLogWarning(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
@ -140,10 +139,10 @@ public class BaseRequestValidatorTests
}
/* Logic path
ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|-> SetErrorResult
*/
* ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
* |-> SetErrorResult
*/
[Theory, BitAutoData]
public async Task ValidateAsync_ContextNotValid_MaxAttemptLogin_ShouldSendEmail(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
@ -177,134 +176,97 @@ public class BaseRequestValidatorTests
Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message);
}
/* Logic path
ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildErrorResult
*/
[Theory, BitAutoData]
public async Task ValidateAsync_AuthCodeGrantType_DeviceNull_ShouldError(
public async Task ValidateAsync_DeviceNotValidated_ShouldLogError(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, default)));
// 1 -> to pass
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.ValidatedTokenRequest.GrantType = "authorization_code";
// 2 -> will result to false with no extra configuration
// 3 -> set two factor to be false
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), tokenRequest)
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
// 4 -> set up device validator to fail
requestContext.KnownDevice = false;
tokenRequest.GrantType = "password";
_deviceValidator.ValidateRequestDeviceAsync(Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>())
.Returns(Task.FromResult(false));
// 5 -> not legacy user
_userService.IsLegacyUser(Arg.Any<string>())
.Returns(false);
// Act
await _sut.ValidateAsync(context);
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
// Assert
Assert.True(context.GrantResult.IsError);
Assert.Equal("No device information provided.", errorResponse.Message);
await _eventService.Received(1)
.LogUserEventAsync(context.CustomValidatorRequestContext.User.Id, EventType.User_FailedLogIn);
}
/* Logic path
ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildSuccessResultAsync
*/
[Theory, BitAutoData]
public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult,
Device device)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1);
_globalSettings.DisableEmailNewDevice = false;
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(device);
// Act
await _sut.ValidateAsync(context);
// Assert
Assert.False(context.GrantResult.IsError);
}
/* Logic path
ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildSuccessResultAsync
*/
[Theory, BitAutoData]
public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_ShouldSucceed(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult,
Device device)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1);
_globalSettings.DisableEmailNewDevice = false;
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(device);
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
// Act
await _sut.ValidateAsync(context);
// Assert
await _eventService.LogUserEventAsync(
context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn);
await _userRepository.Received(1).ReplaceAsync(Arg.Any<User>());
Assert.False(context.GrantResult.IsError);
}
/* Logic path
ValidateAsync -> IsLegacyUser -> BuildErrorResultAsync
*/
[Theory, BitAutoData]
public async Task ValidateAsync_InvalidAuthType_ShouldSetSsoResult(
public async Task ValidateAsync_DeviceValidated_ShouldSucceed(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.ValidatedTokenRequest.Raw["DeviceIdentifier"] = "DeviceIdentifier";
context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken";
context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName";
context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type
// 1 -> to pass
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.ValidatedTokenRequest.GrantType = "";
// 2 -> will result to false with no extra configuration
// 3 -> set two factor to be false
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), tokenRequest)
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
// 4 -> set up device validator to pass
_deviceValidator.ValidateRequestDeviceAsync(Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>())
.Returns(Task.FromResult(true));
// 5 -> not legacy user
_userService.IsLegacyUser(Arg.Any<string>())
.Returns(false);
// Act
await _sut.ValidateAsync(context);
// Assert
Assert.False(context.GrantResult.IsError);
}
// Test grantTypes that require SSO when a user is in an organization that requires it
[Theory]
[BitAutoData("password")]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
public async Task ValidateAsync_GrantTypes_OrgSsoRequiredTrue_ShouldSetSsoResult(
string grantType,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.ValidatedTokenRequest.GrantType = grantType;
_policyService.AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(true));
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
// Act
await _sut.ValidateAsync(context);
@ -314,6 +276,85 @@ public class BaseRequestValidatorTests
Assert.Equal("SSO authentication is required.", errorResponse.Message);
}
// Test grantTypes where SSO would be required but the user is not in an
// organization that requires it
[Theory]
[BitAutoData("password")]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
public async Task ValidateAsync_GrantTypes_OrgSsoRequiredFalse_ShouldSucceed(
string grantType,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.ValidatedTokenRequest.GrantType = grantType;
_policyService.AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(false));
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
context.ValidatedTokenRequest.ClientId = "web";
// Act
await _sut.ValidateAsync(context);
// Assert
await _eventService.Received(1).LogUserEventAsync(
context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn);
await _userRepository.Received(1).ReplaceAsync(Arg.Any<User>());
Assert.False(context.GrantResult.IsError);
}
// Test the grantTypes where SSO is in progress or not relevant
[Theory]
[BitAutoData("authorization_code")]
[BitAutoData("client_credentials")]
public async Task ValidateAsync_GrantTypes_SsoRequiredFalse_ShouldSucceed(
string grantType,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
_sut.isValid = true;
context.ValidatedTokenRequest.GrantType = grantType;
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
context.ValidatedTokenRequest.ClientId = "web";
// Act
await _sut.ValidateAsync(context);
// Assert
await _policyService.DidNotReceive().AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
await _eventService.Received(1).LogUserEventAsync(
context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn);
await _userRepository.Received(1).ReplaceAsync(Arg.Any<User>());
Assert.False(context.GrantResult.IsError);
}
/* Logic Path
* ValidateAsync -> UserService.IsLegacyUser -> FailAuthForLegacyUserAsync
*/
[Theory, BitAutoData]
public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
@ -332,6 +373,8 @@ public class BaseRequestValidatorTests
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
// Act
await _sut.ValidateAsync(context);
@ -339,8 +382,9 @@ public class BaseRequestValidatorTests
// Assert
Assert.True(context.GrantResult.IsError);
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
Assert.Equal($"Encryption key migration is required. Please log in to the web vault at {_globalSettings.BaseServiceUri.VaultWithHash}"
, errorResponse.Message);
var expectedMessage = $"Encryption key migration is required. Please log in to the web " +
$"vault at {_globalSettings.BaseServiceUri.VaultWithHash}";
Assert.Equal(expectedMessage, errorResponse.Message);
}
private BaseRequestValidationContextFake CreateContext(
@ -367,4 +411,12 @@ public class BaseRequestValidatorTests
Substitute.For<IServiceProvider>(),
Substitute.For<ILogger<UserManager<User>>>());
}
private void AddValidDeviceToRequest(ValidatedTokenRequest request)
{
request.Raw["DeviceIdentifier"] = "DeviceIdentifier";
request.Raw["DeviceType"] = "Android"; // must be valid device type
request.Raw["DeviceName"] = "DeviceName";
request.Raw["DevicePushToken"] = "DevicePushToken";
}
}

View File

@ -1,12 +1,17 @@
using Bit.Core.Context;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Identity.IdentityServer;
using Bit.Identity.IdentityServer.RequestValidators;
using Bit.Test.Common.AutoFixture.Attributes;
using Duende.IdentityServer.Validation;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using AuthFixtures = Bit.Identity.Test.AutoFixture;
@ -20,6 +25,10 @@ public class DeviceValidatorTests
private readonly GlobalSettings _globalSettings;
private readonly IMailService _mailService;
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService;
private readonly IDistributedCache _distributedCache;
private readonly Logger<DeviceValidator> _logger;
private readonly IFeatureService _featureService;
private readonly DeviceValidator _sut;
public DeviceValidatorTests()
@ -29,219 +38,582 @@ public class DeviceValidatorTests
_globalSettings = new GlobalSettings();
_mailService = Substitute.For<IMailService>();
_currentContext = Substitute.For<ICurrentContext>();
_userService = Substitute.For<IUserService>();
_distributedCache = Substitute.For<IDistributedCache>();
_logger = new Logger<DeviceValidator>(Substitute.For<ILoggerFactory>());
_featureService = Substitute.For<IFeatureService>();
_sut = new DeviceValidator(
_deviceService,
_deviceRepository,
_globalSettings,
_mailService,
_currentContext);
_currentContext,
_userService,
_distributedCache,
_logger,
_featureService);
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_DeviceNull_ShouldReturnNull(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request.Raw["DeviceIdentifier"] = null;
// Act
var device = await _sut.SaveDeviceAsync(user, request);
// Assert
Assert.Null(device);
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_UserIsNull_ShouldReturnNull(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
request = AddValidDeviceToRequest(request);
// Act
var device = await _sut.SaveDeviceAsync(null, request);
// Assert
Assert.Null(device);
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendsEmail(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request = AddValidDeviceToRequest(request);
user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
_globalSettings.DisableEmailNewDevice = false;
// Act
var device = await _sut.SaveDeviceAsync(user, request);
// Assert
Assert.NotNull(device);
Assert.Equal(user.Id, device.UserId);
Assert.Equal("DeviceIdentifier", device.Identifier);
Assert.Equal(DeviceType.Android, device.Type);
await _mailService.Received(1).SendNewDeviceLoggedInEmail(
user.Email, "Android", Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendEmailFalse(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request = AddValidDeviceToRequest(request);
user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
_globalSettings.DisableEmailNewDevice = true;
// Act
var device = await _sut.SaveDeviceAsync(user, request);
// Assert
Assert.NotNull(device);
Assert.Equal(user.Id, device.UserId);
Assert.Equal("DeviceIdentifier", device.Identifier);
Assert.Equal(DeviceType.Android, device.Type);
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
user.Email, "Android", Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_DeviceIsKnown_ShouldReturnDevice(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user,
[Theory, BitAutoData]
public async void GetKnownDeviceAsync_UserNull_ReturnsFalse(
Device device)
{
// Arrange
request = AddValidDeviceToRequest(request);
device.UserId = user.Id;
device.Identifier = "DeviceIdentifier";
device.Type = DeviceType.Android;
device.Name = "DeviceName";
device.PushToken = "DevicePushToken";
_deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id).Returns(device);
// AutoData arranges
// Act
var resultDevice = await _sut.SaveDeviceAsync(user, request);
var result = await _sut.GetKnownDeviceAsync(null, device);
// Assert
Assert.Equal(device, resultDevice);
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_NewUser_DeviceUnknown_ShouldSaveDevice_NoEmail(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request = AddValidDeviceToRequest(request);
user.CreationDate = DateTime.UtcNow;
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>()).Returns(null as Device);
// Act
var device = await _sut.SaveDeviceAsync(user, request);
// Assert
Assert.NotNull(device);
Assert.Equal(user.Id, device.UserId);
Assert.Equal("DeviceIdentifier", device.Identifier);
Assert.Equal(DeviceType.Android, device.Type);
await _deviceService.Received(1).SaveAsync(device);
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void KnownDeviceAsync_UserNull_ReturnsFalse(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
request = AddValidDeviceToRequest(request);
// Act
var result = await _sut.KnownDeviceAsync(null, request);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async void KnownDeviceAsync_DeviceNull_ReturnsFalse(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
[Theory, BitAutoData]
public async void GetKnownDeviceAsync_DeviceNull_ReturnsFalse(
User user)
{
// Arrange
// Device raw data is null which will cause the device to be null
// Act
var result = await _sut.KnownDeviceAsync(user, request);
var result = await _sut.GetKnownDeviceAsync(user, null);
// Assert
Assert.False(result);
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async void KnownDeviceAsync_DeviceNotInDatabase_ReturnsFalse(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
[Theory, BitAutoData]
public async void GetKnownDeviceAsync_DeviceNotInDatabase_ReturnsFalse(
User user,
Device device)
{
// Arrange
request = AddValidDeviceToRequest(request);
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>())
.Returns(null as Device);
// Act
var result = await _sut.KnownDeviceAsync(user, request);
var result = await _sut.GetKnownDeviceAsync(user, device);
// Assert
Assert.False(result);
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async void KnownDeviceAsync_UserAndDeviceValid_ReturnsTrue(
[Theory, BitAutoData]
public async void GetKnownDeviceAsync_UserAndDeviceValid_ReturnsTrue(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user,
Device device)
{
// Arrange
request = AddValidDeviceToRequest(request);
AddValidDeviceToRequest(request);
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>())
.Returns(device);
// Act
var result = await _sut.KnownDeviceAsync(user, request);
var result = await _sut.GetKnownDeviceAsync(user, device);
// Assert
Assert.NotNull(result);
}
[Theory]
[BitAutoData("not null", "Android", "")]
[BitAutoData("not null", "", "not null")]
[BitAutoData("", "Android", "not null")]
public void GetDeviceFromRequest_RawDeviceInfoNull_ReturnsNull(
string deviceIdentifier,
string deviceType,
string deviceName,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
request.Raw["DeviceIdentifier"] = deviceIdentifier;
request.Raw["DeviceType"] = deviceType;
request.Raw["DeviceName"] = deviceName;
// Act
var result = DeviceValidator.GetDeviceFromRequest(request);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public void GetDeviceFromRequest_RawDeviceInfoValid_ReturnsDevice(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
AddValidDeviceToRequest(request);
// Act
var result = DeviceValidator.GetDeviceFromRequest(request);
// Assert
Assert.NotNull(result);
Assert.Equal("DeviceIdentifier", result.Identifier);
Assert.Equal("DeviceName", result.Name);
Assert.Equal(DeviceType.Android, result.Type);
Assert.Equal("DevicePushToken", result.PushToken);
}
[Theory, BitAutoData]
public async void ValidateRequestDeviceAsync_DeviceNull_ContextModified_ReturnsFalse(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
context.KnownDevice = false;
context.Device = null;
// Act
Assert.NotNull(context.User);
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
Assert.False(result);
Assert.NotNull(context.CustomResponse["ErrorModel"]);
var expectedErrorModel = new ErrorResponseModel("no device information provided");
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
Assert.Equal(expectedErrorModel.Message, actualResponse.Message);
}
[Theory, BitAutoData]
public async void ValidateRequestDeviceAsync_RequestDeviceKnown_ContextDeviceModified_ReturnsTrue(
Device device,
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
context.KnownDevice = false;
context.Device = null;
AddValidDeviceToRequest(request);
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>())
.Returns(device);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
Assert.True(result);
Assert.False(context.CustomResponse.ContainsKey("ErrorModel"));
Assert.NotNull(context.Device);
Assert.Equal(context.Device, device);
}
[Theory, BitAutoData]
public async void ValidateRequestDeviceAsync_ContextDeviceKnown_ContextDeviceModified_ReturnsTrue(
Device databaseDevice,
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
context.KnownDevice = false;
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>())
.Returns(databaseDevice);
// we want to show that the context device is updated when the device is known
Assert.NotEqual(context.Device, databaseDevice);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
Assert.True(result);
Assert.False(context.CustomResponse.ContainsKey("ErrorModel"));
Assert.Equal(context.Device, databaseDevice);
}
[Theory, BitAutoData]
public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_SendsEmail_ReturnsTrue(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
context.KnownDevice = false;
AddValidDeviceToRequest(request);
_globalSettings.DisableEmailNewDevice = false;
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
.Returns(null as Device);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
.Returns(false);
// set user creation to more than 10 minutes ago
context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(1).SaveAsync(context.Device);
await _mailService.Received(1).SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
Assert.True(result);
}
[Theory, BitAutoData]
public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_NewUser_DoesNotSendEmail_ReturnsTrue(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
context.KnownDevice = false;
AddValidDeviceToRequest(request);
_globalSettings.DisableEmailNewDevice = false;
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
.Returns(null as Device);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
.Returns(false);
// set user creation to less than 10 minutes ago
context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(9);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(1).SaveAsync(context.Device);
await _mailService.Received(0).SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
Assert.True(result);
}
[Theory, BitAutoData]
public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_DisableEmailTrue_DoesNotSendEmail_ReturnsTrue(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
context.KnownDevice = false;
AddValidDeviceToRequest(request);
_globalSettings.DisableEmailNewDevice = true;
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
.Returns(null as Device);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
.Returns(false);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(1).SaveAsync(context.Device);
await _mailService.Received(0).SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
Assert.True(result);
}
[Theory]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
[BitAutoData("authorization_code")]
[BitAutoData("client_credentials")]
public async void ValidateRequestDeviceAsync_GrantTypeNotPassword_SavesDevice_ReturnsTrue(
string grantType,
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
context.KnownDevice = false;
ArrangeForHandleNewDeviceVerificationTest(context, request);
AddValidDeviceToRequest(request);
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
.Returns(null as Device);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
.Returns(true);
request.GrantType = grantType;
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(1).SaveAsync(context.Device);
Assert.True(result);
}
[Theory, BitAutoData]
public async void ValidateRequestDeviceAsync_IsAuthRequest_SavesDevice_ReturnsTrue(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
context.KnownDevice = false;
ArrangeForHandleNewDeviceVerificationTest(context, request);
AddValidDeviceToRequest(request);
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
.Returns(null as Device);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
.Returns(true);
request.Raw.Add("AuthRequest", "authRequest");
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(1).SaveAsync(context.Device);
Assert.True(result);
}
[Theory, BitAutoData]
public async void ValidateRequestDeviceAsync_TwoFactorRequired_SavesDevice_ReturnsTrue(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
context.KnownDevice = false;
ArrangeForHandleNewDeviceVerificationTest(context, request);
AddValidDeviceToRequest(request);
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
.Returns(null as Device);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
.Returns(true);
context.TwoFactorRequired = true;
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(1).SaveAsync(context.Device);
Assert.True(result);
}
[Theory, BitAutoData]
public async void ValidateRequestDeviceAsync_SsoRequired_SavesDevice_ReturnsTrue(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
context.KnownDevice = false;
ArrangeForHandleNewDeviceVerificationTest(context, request);
AddValidDeviceToRequest(request);
_deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id)
.Returns(null as Device);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)
.Returns(true);
context.SsoRequired = true;
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(1).SaveAsync(context.Device);
Assert.True(result);
}
[Theory, BitAutoData]
public async void HandleNewDeviceVerificationAsync_UserNull_ContextModified_ReturnsInvalidUser(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
ArrangeForHandleNewDeviceVerificationTest(context, request);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
_globalSettings.EnableNewDeviceVerification = true;
context.User = null;
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
Assert.False(result);
Assert.NotNull(context.CustomResponse["ErrorModel"]);
// PM-13340: The error message should be "invalid user" instead of "no device information provided"
var expectedErrorMessage = "no device information provided";
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
Assert.Equal(expectedErrorMessage, actualResponse.Message);
}
[Theory, BitAutoData]
public async void HandleNewDeviceVerificationAsync_UserHasCacheValue_ReturnsSuccess(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
ArrangeForHandleNewDeviceVerificationTest(context, request);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
_globalSettings.EnableNewDeviceVerification = true;
_distributedCache.GetAsync(Arg.Any<string>()).Returns([1]);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _userService.Received(0).SendOTPAsync(context.User);
await _deviceService.Received(1).SaveAsync(Arg.Any<Device>());
Assert.True(result);
Assert.False(context.CustomResponse.ContainsKey("ErrorModel"));
Assert.Equal(context.User.Id, context.Device.UserId);
Assert.NotNull(context.Device);
}
[Theory, BitAutoData]
public async void HandleNewDeviceVerificationAsync_NewDeviceOtpValid_ReturnsSuccess(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
ArrangeForHandleNewDeviceVerificationTest(context, request);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
_globalSettings.EnableNewDeviceVerification = true;
_distributedCache.GetAsync(Arg.Any<string>()).Returns(null as byte[]);
var newDeviceOtp = "123456";
request.Raw.Add("NewDeviceOtp", newDeviceOtp);
_userService.VerifyOTPAsync(context.User, newDeviceOtp).Returns(true);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _userService.Received(0).SendOTPAsync(context.User);
await _deviceService.Received(1).SaveAsync(context.Device);
Assert.True(result);
Assert.False(context.CustomResponse.ContainsKey("ErrorModel"));
Assert.Equal(context.User.Id, context.Device.UserId);
Assert.NotNull(context.Device);
}
[Theory]
[BitAutoData("")]
[BitAutoData("123456")]
public async void HandleNewDeviceVerificationAsync_NewDeviceOtpInvalid_ReturnsInvalidNewDeviceOtp(
string newDeviceOtp,
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
ArrangeForHandleNewDeviceVerificationTest(context, request);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
_globalSettings.EnableNewDeviceVerification = true;
_distributedCache.GetAsync(Arg.Any<string>()).Returns(null as byte[]);
request.Raw.Add("NewDeviceOtp", newDeviceOtp);
_userService.VerifyOTPAsync(context.User, newDeviceOtp).Returns(false);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _userService.DidNotReceive().SendOTPAsync(Arg.Any<User>());
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
Assert.False(result);
Assert.NotNull(context.CustomResponse["ErrorModel"]);
var expectedErrorMessage = "invalid new device otp";
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
Assert.Equal(expectedErrorMessage, actualResponse.Message);
}
[Theory, BitAutoData]
public async void HandleNewDeviceVerificationAsync_UserHasNoDevices_ReturnsSuccess(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
ArrangeForHandleNewDeviceVerificationTest(context, request);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
_globalSettings.EnableNewDeviceVerification = true;
_distributedCache.GetAsync(Arg.Any<string>()).Returns([1]);
_deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([]);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _userService.Received(0).VerifyOTPAsync(Arg.Any<User>(), Arg.Any<string>());
await _userService.Received(0).SendOTPAsync(Arg.Any<User>());
await _deviceService.Received(1).SaveAsync(context.Device);
Assert.True(result);
Assert.False(context.CustomResponse.ContainsKey("ErrorModel"));
Assert.Equal(context.User.Id, context.Device.UserId);
Assert.NotNull(context.Device);
}
[Theory, BitAutoData]
public async void HandleNewDeviceVerificationAsync_NewDeviceOtpEmpty_UserHasDevices_ReturnsNewDeviceVerificationRequired(
CustomValidatorRequestContext context,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
ArrangeForHandleNewDeviceVerificationTest(context, request);
_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true);
_globalSettings.EnableNewDeviceVerification = true;
_deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([new Device()]);
_distributedCache.GetAsync(Arg.Any<string>()).Returns(null as byte[]);
// Act
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _userService.Received(1).SendOTPAsync(context.User);
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
Assert.False(result);
Assert.NotNull(context.CustomResponse["ErrorModel"]);
var expectedErrorMessage = "new device verification required";
var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"];
Assert.Equal(expectedErrorMessage, actualResponse.Message);
}
[Theory, BitAutoData]
public void NewDeviceOtpRequest_NewDeviceOtpNull_ReturnsFalse(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
// Autodata arranges
// Act
var result = DeviceValidator.NewDeviceOtpRequest(request);
// Assert
Assert.False(result);
}
[Theory, BitAutoData]
public void NewDeviceOtpRequest_NewDeviceOtpNotNull_ReturnsTrue(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
request.Raw["NewDeviceOtp"] = "123456";
// Act
var result = DeviceValidator.NewDeviceOtpRequest(request);
// Assert
Assert.True(result);
}
private ValidatedTokenRequest AddValidDeviceToRequest(ValidatedTokenRequest request)
private static void AddValidDeviceToRequest(ValidatedTokenRequest request)
{
request.Raw["DeviceIdentifier"] = "DeviceIdentifier";
request.Raw["DeviceType"] = "Android";
request.Raw["DeviceType"] = "Android"; // must be valid device type
request.Raw["DeviceName"] = "DeviceName";
request.Raw["DevicePushToken"] = "DevicePushToken";
return request;
}
/// <summary>
/// Configures the request context to facilitate testing the HandleNewDeviceVerificationAsync method.
/// </summary>
/// <param name="context">test context</param>
/// <param name="request">test request</param>
private static void ArrangeForHandleNewDeviceVerificationTest(
CustomValidatorRequestContext context,
ValidatedTokenRequest request)
{
context.KnownDevice = false;
request.GrantType = "password";
context.TwoFactorRequired = false;
context.SsoRequired = false;
}
}

View File

@ -123,6 +123,11 @@ IBaseRequestValidatorTestWrapper
Dictionary<string, object> customResponse)
{ }
protected override void SetValidationErrorResult(
BaseRequestValidationContextFake context,
CustomValidatorRequestContext requestContext)
{ }
protected override Task<bool> ValidateContextAsync(
BaseRequestValidationContextFake context,
CustomValidatorRequestContext validatorContext)

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -255,9 +255,8 @@ public class OrganizationUserRepositoryTests
Assert.Equal(organization.SmServiceAccounts, result.SmServiceAccounts);
Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation);
Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion);
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
Assert.Equal(organization.LimitCollectionCreationDeletion, result.LimitCollectionCreationDeletion);
Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems);
Assert.Equal(organization.UseRiskInsights, result.UseRiskInsights);
}
[DatabaseTheory, DatabaseData]

View File

@ -0,0 +1,99 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.Repositories;
public class UserRepositoryTests
{
[DatabaseTheory, DatabaseData]
public async Task DeleteAsync_Works(IUserRepository userRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
await userRepository.DeleteAsync(user);
var deletedUser = await userRepository.GetByIdAsync(user.Id);
Assert.Null(deletedUser);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteManyAsync_Works(IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository)
{
var user1 = await userRepository.CreateAsync(new User
{
Name = "Test User 1",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var user2 = await userRepository.CreateAsync(new User
{
Name = "Test User 2",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var user3 = await userRepository.CreateAsync(new User
{
Name = "Test User 3",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user3.Email, // TODO: EF does not enfore this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
});
await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
});
await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user3.Id,
Status = OrganizationUserStatusType.Confirmed,
});
await userRepository.DeleteManyAsync(new List<User>
{
user1,
user2
});
var deletedUser1 = await userRepository.GetByIdAsync(user1.Id);
var deletedUser2 = await userRepository.GetByIdAsync(user2.Id);
var notDeletedUser3 = await userRepository.GetByIdAsync(user3.Id);
var orgUser1Deleted = await organizationUserRepository.GetByIdAsync(user1.Id);
var notDeletedOrgUsers = await organizationUserRepository.GetManyByUserAsync(user3.Id);
Assert.Null(deletedUser1);
Assert.Null(deletedUser2);
Assert.NotNull(notDeletedUser3);
Assert.Null(orgUser1Deleted);
Assert.NotNull(notDeletedOrgUsers);
Assert.True(notDeletedOrgUsers.Count > 0);
}
}

View File

@ -0,0 +1,22 @@
using System.Diagnostics.CodeAnalysis;
using Bit.Core.Vault.Entities;
namespace Bit.Infrastructure.IntegrationTest.Comparers;
/// <summary>
/// Determines the equality of two SecurityTask objects.
/// </summary>
public class SecurityTaskComparer : IEqualityComparer<SecurityTask>
{
public bool Equals(SecurityTask x, SecurityTask y)
{
return x.Id.Equals(y.Id) &&
x.Type.Equals(y.Type) &&
x.Status.Equals(y.Status);
}
public int GetHashCode([DisallowNull] SecurityTask obj)
{
return base.GetHashCode();
}
}

View File

@ -1,9 +1,13 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.IntegrationTest.Comparers;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.Vault.Repositories;
@ -120,4 +124,103 @@ public class SecurityTaskRepositoryTests
Assert.Equal(task.Id, updatedTask.Id);
Assert.Equal(SecurityTaskStatus.Completed, updatedTask.Status);
}
[DatabaseTheory, DatabaseData]
public async Task GetManyByUserIdAsync_ReturnsExpectedTasks(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
ICipherRepository cipherRepository,
ISecurityTaskRepository securityTaskRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Test Plan",
BillingEmail = "billing@email.com"
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed
});
var collection = await collectionRepository.CreateAsync(new Collection
{
OrganizationId = organization.Id,
Name = "Test Collection 1",
});
var collection2 = await collectionRepository.CreateAsync(new Collection
{
OrganizationId = organization.Id,
Name = "Test Collection 2",
});
var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", };
await cipherRepository.CreateAsync(cipher1, [collection.Id, collection2.Id]);
var cipher2 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", };
await cipherRepository.CreateAsync(cipher2, [collection.Id]);
var task1 = await securityTaskRepository.CreateAsync(new SecurityTask
{
OrganizationId = organization.Id,
CipherId = cipher1.Id,
Status = SecurityTaskStatus.Pending,
Type = SecurityTaskType.UpdateAtRiskCredential,
});
var task2 = await securityTaskRepository.CreateAsync(new SecurityTask
{
OrganizationId = organization.Id,
CipherId = cipher2.Id,
Status = SecurityTaskStatus.Completed,
Type = SecurityTaskType.UpdateAtRiskCredential,
});
var task3 = await securityTaskRepository.CreateAsync(new SecurityTask
{
OrganizationId = organization.Id,
CipherId = cipher2.Id,
Status = SecurityTaskStatus.Pending,
Type = SecurityTaskType.UpdateAtRiskCredential,
});
await collectionRepository.UpdateUsersAsync(collection.Id,
new List<CollectionAccessSelection>
{
new() {Id = orgUser.Id, ReadOnly = false, HidePasswords = false, Manage = true}
});
var allTasks = await securityTaskRepository.GetManyByUserIdStatusAsync(user.Id);
Assert.Equal(3, allTasks.Count);
Assert.Contains(task1, allTasks, new SecurityTaskComparer());
Assert.Contains(task2, allTasks, new SecurityTaskComparer());
Assert.Contains(task3, allTasks, new SecurityTaskComparer());
var pendingTasks = await securityTaskRepository.GetManyByUserIdStatusAsync(user.Id, SecurityTaskStatus.Pending);
Assert.Equal(2, pendingTasks.Count);
Assert.Contains(task1, pendingTasks, new SecurityTaskComparer());
Assert.Contains(task3, pendingTasks, new SecurityTaskComparer());
Assert.DoesNotContain(task2, pendingTasks, new SecurityTaskComparer());
var completedTasks = await securityTaskRepository.GetManyByUserIdStatusAsync(user.Id, SecurityTaskStatus.Completed);
Assert.Single(completedTasks);
Assert.Contains(task2, completedTasks, new SecurityTaskComparer());
Assert.DoesNotContain(task1, completedTasks, new SecurityTaskComparer());
Assert.DoesNotContain(task3, completedTasks, new SecurityTaskComparer());
}
}