1
0
mirror of https://github.com/bitwarden/server synced 2026-01-09 20:13:24 +00:00

Merge branch 'main' of github.com:bitwarden/server into arch/seeder-sdk

# Conflicts:
#	.gitignore
#	bitwarden-server.sln
This commit is contained in:
Hinton
2025-10-09 09:46:13 -07:00
1858 changed files with 162611 additions and 13544 deletions

View File

@@ -0,0 +1,477 @@
using System.Net;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.Repositories;
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.Services;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private static readonly string _mockEncryptedString =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
public OrganizationUserControllerTests(ApiApplicationFactory apiFactory)
{
_factory = apiFactory;
_factory.SubstituteService<IFeatureService>(featureService =>
{
featureService
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
});
_client = _factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private Organization _organization = null!;
private string _ownerEmail = null!;
[Fact]
public async Task BulkDeleteAccount_Success()
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Owner);
await _loginHelper.LoginAsync(userEmail);
var (_, orgUserToDelete) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com");
var userRepository = _factory.GetService<IUserRepository>();
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
Assert.NotNull(orgUserToDelete.UserId);
Assert.NotNull(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));
Assert.NotNull(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));
var request = new OrganizationUserBulkRequestModel
{
Ids = [orgUserToDelete.Id]
};
var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/delete-account", request);
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
Assert.Single(content.Data, r => r.Id == orgUserToDelete.Id && r.Error == string.Empty);
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.Null(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));
Assert.Null(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));
}
[Fact]
public async Task BulkDeleteAccount_MixedResults()
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Admin);
await _loginHelper.LoginAsync(userEmail);
// Can delete users
var (_, validOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
// Cannot delete owners
var (_, invalidOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.Owner);
await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com");
var userRepository = _factory.GetService<IUserRepository>();
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
Assert.NotNull(validOrgUser.UserId);
Assert.NotNull(invalidOrgUser.UserId);
var arrangedUsers =
await userRepository.GetManyAsync([validOrgUser.UserId.Value, invalidOrgUser.UserId.Value]);
Assert.Equal(2, arrangedUsers.Count());
var arrangedOrgUsers =
await organizationUserRepository.GetManyAsync([validOrgUser.Id, invalidOrgUser.Id]);
Assert.Equal(2, arrangedOrgUsers.Count);
var request = new OrganizationUserBulkRequestModel
{
Ids = [validOrgUser.Id, invalidOrgUser.Id]
};
var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/delete-account", request);
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
var debug = await httpResponse.Content.ReadAsStringAsync();
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
Assert.Equal(2, content.Data.Count());
Assert.Contains(content.Data, r => r.Id == validOrgUser.Id && r.Error == string.Empty);
Assert.Contains(content.Data, r =>
r.Id == invalidOrgUser.Id &&
string.Equals(r.Error, new CannotDeleteOwnersError().Message, StringComparison.Ordinal));
var actualUsers =
await userRepository.GetManyAsync([validOrgUser.UserId.Value, invalidOrgUser.UserId.Value]);
Assert.Single(actualUsers, u => u.Id == invalidOrgUser.UserId.Value);
var actualOrgUsers =
await organizationUserRepository.GetManyAsync([validOrgUser.Id, invalidOrgUser.Id]);
Assert.Single(actualOrgUsers, ou => ou.Id == invalidOrgUser.Id);
}
[Theory]
[InlineData(OrganizationUserType.User)]
[InlineData(OrganizationUserType.Custom)]
public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ReturnsForbiddenResponse(OrganizationUserType organizationUserType)
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, organizationUserType, new Permissions { ManageUsers = false });
await _loginHelper.LoginAsync(userEmail);
var request = new OrganizationUserBulkRequestModel
{
Ids = new List<Guid> { Guid.NewGuid() }
};
var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/delete-account", request);
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
}
[Fact]
public async Task DeleteAccount_Success()
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Owner);
await _loginHelper.LoginAsync(userEmail);
var (_, orgUserToDelete) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com");
var userRepository = _factory.GetService<IUserRepository>();
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
Assert.NotNull(orgUserToDelete.UserId);
Assert.NotNull(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));
Assert.NotNull(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));
var httpResponse = await _client.DeleteAsync($"organizations/{_organization.Id}/users/{orgUserToDelete.Id}/delete-account");
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.Null(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));
Assert.Null(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));
}
[Theory]
[InlineData(OrganizationUserType.User)]
[InlineData(OrganizationUserType.Custom)]
public async Task DeleteAccount_WhenUserCannotManageUsers_ReturnsForbiddenResponse(OrganizationUserType organizationUserType)
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, organizationUserType, new Permissions { ManageUsers = false });
await _loginHelper.LoginAsync(userEmail);
var userToRemove = Guid.NewGuid();
var httpResponse = await _client.DeleteAsync($"organizations/{_organization.Id}/users/{userToRemove}/delete-account");
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
}
[Theory]
[InlineData(OrganizationUserType.User)]
[InlineData(OrganizationUserType.Custom)]
public async Task GetAccountRecoveryDetails_WithoutManageResetPasswordPermission_ReturnsForbiddenResponse(OrganizationUserType organizationUserType)
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, organizationUserType, new Permissions { ManageUsers = false });
await _loginHelper.LoginAsync(userEmail);
var request = new OrganizationUserBulkRequestModel
{
Ids = []
};
var httpResponse =
await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/account-recovery-details", request);
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
}
public async Task InitializeAsync()
{
_ownerEmail = $"org-user-integration-test-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
}
[Fact]
public async Task Confirm_WithValidUser_ReturnsSuccess()
{
await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);
var acceptedOrgUser = (await CreateAcceptedUsersAsync(new[] { ("test1@bitwarden.com", OrganizationUserType.User) })).First();
await _loginHelper.LoginAsync(_ownerEmail);
var confirmModel = new OrganizationUserConfirmRequestModel
{
Key = "test-key",
DefaultUserCollectionName = _mockEncryptedString
};
var confirmResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/{acceptedOrgUser.Id}/confirm", confirmModel);
Assert.Equal(HttpStatusCode.OK, confirmResponse.StatusCode);
await VerifyUserConfirmedAsync(acceptedOrgUser, "test-key");
await VerifyDefaultCollectionCountAsync(acceptedOrgUser, 1);
}
[Fact]
public async Task Confirm_WithValidOwner_ReturnsSuccess()
{
await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);
var acceptedOrgUser = (await CreateAcceptedUsersAsync(new[] { ("owner1@bitwarden.com", OrganizationUserType.Owner) })).First();
await _loginHelper.LoginAsync(_ownerEmail);
var confirmModel = new OrganizationUserConfirmRequestModel
{
Key = "test-key",
DefaultUserCollectionName = _mockEncryptedString
};
var confirmResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/{acceptedOrgUser.Id}/confirm", confirmModel);
Assert.Equal(HttpStatusCode.OK, confirmResponse.StatusCode);
await VerifyUserConfirmedAsync(acceptedOrgUser, "test-key");
await VerifyDefaultCollectionCountAsync(acceptedOrgUser, 0);
}
[Fact]
public async Task BulkConfirm_WithValidUsers_ReturnsSuccess()
{
const string testKeyFormat = "test-key-{0}";
await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);
var acceptedUsers = await CreateAcceptedUsersAsync([
("test1@example.com", OrganizationUserType.User),
("test2@example.com", OrganizationUserType.Owner),
("test3@example.com", OrganizationUserType.User)
]);
await _loginHelper.LoginAsync(_ownerEmail);
var bulkConfirmModel = new OrganizationUserBulkConfirmRequestModel
{
Keys = acceptedUsers.Select((organizationUser, index) => new OrganizationUserBulkConfirmRequestModelEntry
{
Id = organizationUser.Id,
Key = string.Format(testKeyFormat, index)
}),
DefaultUserCollectionName = _mockEncryptedString
};
var bulkConfirmResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/confirm", bulkConfirmModel);
Assert.Equal(HttpStatusCode.OK, bulkConfirmResponse.StatusCode);
await VerifyMultipleUsersConfirmedAsync(acceptedUsers.Select((organizationUser, index) =>
(organizationUser, string.Format(testKeyFormat, index))).ToList());
await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(0), 1);
await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(1), 0); // Owner does not get a default collection
await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(2), 1);
}
[Fact]
public async Task Put_WithExistingDefaultCollection_Success()
{
// Arrange
await _loginHelper.LoginAsync(_ownerEmail);
var (userEmail, organizationUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.User);
var (group, sharedCollection, defaultCollection) = await CreateTestDataAsync();
await AssignDefaultCollectionToUserAsync(organizationUser, defaultCollection);
// Act
var updateRequest = CreateUpdateRequest(sharedCollection, group);
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/{organizationUser.Id}", updateRequest);
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
// Assert
await VerifyUserWasUpdatedCorrectlyAsync(organizationUser, expectedType: OrganizationUserType.Custom, expectedManageGroups: true);
await VerifyGroupAccessWasAddedAsync(organizationUser, [group]);
await VerifyCollectionAccessWasUpdatedCorrectlyAsync(organizationUser, sharedCollection.Id, defaultCollection.Id);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
private async Task<(Group group, Collection sharedCollection, Collection defaultCollection)> CreateTestDataAsync()
{
var groupRepository = _factory.GetService<IGroupRepository>();
var group = await groupRepository.CreateAsync(new Group
{
OrganizationId = _organization.Id,
Name = $"Test Group {Guid.NewGuid()}"
});
var collectionRepository = _factory.GetService<ICollectionRepository>();
var sharedCollection = await collectionRepository.CreateAsync(new Collection
{
OrganizationId = _organization.Id,
Name = $"Test Collection {Guid.NewGuid()}",
Type = CollectionType.SharedCollection
});
var defaultCollection = await collectionRepository.CreateAsync(new Collection
{
OrganizationId = _organization.Id,
Name = $"My Items {Guid.NewGuid()}",
Type = CollectionType.DefaultUserCollection
});
return (group, sharedCollection, defaultCollection);
}
private async Task AssignDefaultCollectionToUserAsync(OrganizationUser organizationUser, Collection defaultCollection)
{
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
await organizationUserRepository.ReplaceAsync(organizationUser,
new List<CollectionAccessSelection>
{
new CollectionAccessSelection
{
Id = defaultCollection.Id,
ReadOnly = false,
HidePasswords = false,
Manage = true
}
});
}
private static OrganizationUserUpdateRequestModel CreateUpdateRequest(Collection sharedCollection, Group group)
{
return new OrganizationUserUpdateRequestModel
{
Type = OrganizationUserType.Custom,
Permissions = new Permissions
{
ManageGroups = true
},
Collections = new List<SelectionReadOnlyRequestModel>
{
new SelectionReadOnlyRequestModel
{
Id = sharedCollection.Id,
ReadOnly = true,
HidePasswords = false,
Manage = false
}
},
Groups = new List<Guid> { group.Id }
};
}
private async Task VerifyUserWasUpdatedCorrectlyAsync(
OrganizationUser organizationUser,
OrganizationUserType expectedType,
bool expectedManageGroups)
{
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
var updatedOrgUser = await organizationUserRepository.GetByIdAsync(organizationUser.Id);
Assert.NotNull(updatedOrgUser);
Assert.Equal(expectedType, updatedOrgUser.Type);
Assert.Equal(expectedManageGroups, updatedOrgUser.GetPermissions().ManageGroups);
}
private async Task VerifyGroupAccessWasAddedAsync(
OrganizationUser organizationUser, IEnumerable<Group> groups)
{
var groupRepository = _factory.GetService<IGroupRepository>();
var userGroups = await groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);
Assert.All(groups, group => Assert.Contains(group.Id, userGroups));
}
private async Task VerifyCollectionAccessWasUpdatedCorrectlyAsync(
OrganizationUser organizationUser, Guid sharedCollectionId, Guid defaultCollectionId)
{
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
var (_, collectionAccess) = await organizationUserRepository.GetByIdWithCollectionsAsync(organizationUser.Id);
var collectionIds = collectionAccess.Select(c => c.Id).ToHashSet();
Assert.Contains(defaultCollectionId, collectionIds);
Assert.Contains(sharedCollectionId, collectionIds);
var newCollectionAccess = collectionAccess.First(c => c.Id == sharedCollectionId);
Assert.True(newCollectionAccess.ReadOnly);
Assert.False(newCollectionAccess.HidePasswords);
Assert.False(newCollectionAccess.Manage);
}
private async Task<List<OrganizationUser>> CreateAcceptedUsersAsync(
IEnumerable<(string email, OrganizationUserType userType)> newUsers)
{
var acceptedUsers = new List<OrganizationUser>();
foreach (var (email, userType) in newUsers)
{
await _factory.LoginWithNewAccount(email);
var acceptedOrgUser = await OrganizationTestHelpers.CreateUserAsync(
_factory, _organization.Id, email,
userType, userStatusType: OrganizationUserStatusType.Accepted);
acceptedUsers.Add(acceptedOrgUser);
}
return acceptedUsers;
}
private async Task VerifyDefaultCollectionCountAsync(OrganizationUser orgUser, int expectedCount)
{
var collectionRepository = _factory.GetService<ICollectionRepository>();
var collections = await collectionRepository.GetManyByUserIdAsync(orgUser.UserId!.Value);
Assert.Equal(expectedCount, collections.Count);
}
private async Task VerifyUserConfirmedAsync(OrganizationUser orgUser, string expectedKey)
{
await VerifyMultipleUsersConfirmedAsync(new List<(OrganizationUser orgUser, string key)> { (orgUser, expectedKey) });
}
private async Task VerifyMultipleUsersConfirmedAsync(List<(OrganizationUser orgUser, string key)> acceptedOrganizationUsers)
{
var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();
for (int i = 0; i < acceptedOrganizationUsers.Count; i++)
{
var confirmedUser = await orgUserRepository.GetByIdAsync(acceptedOrganizationUsers[i].orgUser.Id);
Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);
Assert.Equal(acceptedOrganizationUsers[i].key, confirmedUser.Key);
}
}
}

View File

@@ -0,0 +1,214 @@
using System.Net;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request;
using Bit.Api.AdminConsole.Models.Response.Organizations;
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.Core.Repositories;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private Organization _organization = null!;
private string _ownerEmail = null!;
public PoliciesControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_factory.SubstituteService<Core.Services.IFeatureService>(featureService =>
{
featureService
.IsEnabled("pm-19467-create-default-location")
.Returns(true);
});
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
public async Task InitializeAsync()
{
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
await _loginHelper.LoginAsync(_ownerEmail);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
public async Task PutVNext_OrganizationDataOwnershipPolicy_Success()
{
// Arrange
const PolicyType policyType = PolicyType.OrganizationDataOwnership;
const string defaultCollectionName = "Test Default Collection";
var request = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
},
Metadata = new Dictionary<string, object>
{
{ "defaultUserCollectionName", defaultCollectionName }
}
};
var (_, admin) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Admin);
var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.User);
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext",
JsonContent.Create(request));
// Assert
await AssertResponse();
await AssertPolicy();
await AssertDefaultCollectionCreatedOnlyForUserTypeAsync();
return;
async Task AssertDefaultCollectionCreatedOnlyForUserTypeAsync()
{
var collectionRepository = _factory.GetService<ICollectionRepository>();
await AssertUserExpectations(collectionRepository);
await AssertAdminExpectations(collectionRepository);
}
async Task AssertUserExpectations(ICollectionRepository collectionRepository)
{
var collections = await collectionRepository.GetManyByUserIdAsync(user.UserId!.Value);
var defaultCollection = collections.FirstOrDefault(c => c.Name == defaultCollectionName);
Assert.NotNull(defaultCollection);
Assert.Equal(_organization.Id, defaultCollection.OrganizationId);
}
async Task AssertAdminExpectations(ICollectionRepository collectionRepository)
{
var collections = await collectionRepository.GetManyByUserIdAsync(admin.UserId!.Value);
var defaultCollection = collections.FirstOrDefault(c => c.Name == defaultCollectionName);
Assert.Null(defaultCollection);
}
async Task AssertResponse()
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();
Assert.True(content.Enabled);
Assert.Equal(policyType, content.Type);
Assert.Equal(_organization.Id, content.OrganizationId);
}
async Task AssertPolicy()
{
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.Null(policy.Data);
Assert.Equal(_organization.Id, policy.OrganizationId);
}
}
[Fact]
public async Task PutVNext_MasterPasswordPolicy_Success()
{
// Arrange
var policyType = PolicyType.MasterPassword;
var request = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
Data = new Dictionary<string, object>
{
{ "minComplexity", 10 },
{ "minLength", 12 },
{ "requireUpper", true },
{ "requireLower", false },
{ "requireNumbers", true },
{ "requireSpecial", false },
{ "enforceOnLogin", true }
}
}
};
// Act
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext",
JsonContent.Create(request));
// Assert
await AssertResponse();
await AssertPolicyDataForMasterPasswordPolicy();
return;
async Task AssertPolicyDataForMasterPasswordPolicy()
{
var policyRepository = _factory.GetService<IPolicyRepository>();
var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);
AssertPolicy(policy);
AssertMasterPasswordPolicyData(policy);
}
async Task AssertResponse()
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();
Assert.True(content.Enabled);
Assert.Equal(policyType, content.Type);
Assert.Equal(_organization.Id, content.OrganizationId);
}
void AssertPolicy(Policy policy)
{
Assert.NotNull(policy);
Assert.True(policy.Enabled);
Assert.Equal(policyType, policy.Type);
Assert.Equal(_organization.Id, policy.OrganizationId);
Assert.NotNull(policy.Data);
}
void AssertMasterPasswordPolicyData(Policy policy)
{
var resultData = policy.GetDataModel<MasterPasswordPolicyData>();
var json = JsonSerializer.Serialize(request.Policy.Data);
var expectedData = JsonSerializer.Deserialize<MasterPasswordPolicyData>(json);
AssertHelper.AssertPropertyEqual(resultData, expectedData);
}
}
}

View File

@@ -0,0 +1,331 @@
using System.Net;
using Bit.Api.AdminConsole.Public.Models.Request;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Import;
public class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private Organization _organization = null!;
private string _ownerEmail = null!;
public ImportOrganizationUsersAndGroupsCommandTests(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 Import_Existing_Organization_User_Succeeds()
{
var (email, ou) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
OrganizationUserType.User);
var externalId = Guid.NewGuid().ToString();
var request = new OrganizationImportRequestModel();
request.LargeImport = false;
request.OverwriteExisting = false;
request.Groups = [];
request.Members = [
new OrganizationImportRequestModel.OrganizationImportMemberRequestModel
{
Email = email,
ExternalId = externalId,
Deleted = false
}
];
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Assert against the database values
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
var orgUser = await organizationUserRepository.GetByIdAsync(ou.Id);
Assert.NotNull(orgUser);
Assert.Equal(ou.Id, orgUser.Id);
Assert.Equal(email, orgUser.Email);
Assert.Equal(OrganizationUserType.User, orgUser.Type);
Assert.Equal(externalId, orgUser.ExternalId);
Assert.Equal(OrganizationUserStatusType.Confirmed, orgUser.Status);
Assert.Equal(_organization.Id, orgUser.OrganizationId);
}
[Fact]
public async Task Import_New_Organization_User_Succeeds()
{
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(email);
var externalId = Guid.NewGuid().ToString();
var request = new OrganizationImportRequestModel();
request.LargeImport = false;
request.OverwriteExisting = false;
request.Groups = [];
request.Members = [
new OrganizationImportRequestModel.OrganizationImportMemberRequestModel
{
Email = email,
ExternalId = externalId,
Deleted = false
}
];
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Assert against the database values
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
var orgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, email);
Assert.NotNull(orgUser);
Assert.Equal(email, orgUser.Email);
Assert.Equal(OrganizationUserType.User, orgUser.Type);
Assert.Equal(externalId, orgUser.ExternalId);
Assert.Equal(OrganizationUserStatusType.Invited, orgUser.Status);
Assert.Equal(_organization.Id, orgUser.OrganizationId);
}
[Fact]
public async Task Import_New_And_Existing_Organization_Users_Succeeds()
{
// Existing organization user
var (existingEmail, ou) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
OrganizationUserType.User);
var existingExternalId = Guid.NewGuid().ToString();
// New organization user
var newEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(newEmail);
var newExternalId = Guid.NewGuid().ToString();
var request = new OrganizationImportRequestModel();
request.LargeImport = false;
request.OverwriteExisting = false;
request.Groups = [];
request.Members = [
new OrganizationImportRequestModel.OrganizationImportMemberRequestModel
{
Email = existingEmail,
ExternalId = existingExternalId,
Deleted = false
},
new OrganizationImportRequestModel.OrganizationImportMemberRequestModel
{
Email = newEmail,
ExternalId = newExternalId,
Deleted = false
}
];
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Assert against the database values
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
// Existing user
var existingOrgUser = await organizationUserRepository.GetByIdAsync(ou.Id);
Assert.NotNull(existingOrgUser);
Assert.Equal(existingEmail, existingOrgUser.Email);
Assert.Equal(OrganizationUserType.User, existingOrgUser.Type);
Assert.Equal(existingExternalId, existingOrgUser.ExternalId);
Assert.Equal(OrganizationUserStatusType.Confirmed, existingOrgUser.Status);
Assert.Equal(_organization.Id, existingOrgUser.OrganizationId);
// New User
var newOrgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, newEmail);
Assert.NotNull(newOrgUser);
Assert.Equal(newEmail, newOrgUser.Email);
Assert.Equal(OrganizationUserType.User, newOrgUser.Type);
Assert.Equal(newExternalId, newOrgUser.ExternalId);
Assert.Equal(OrganizationUserStatusType.Invited, newOrgUser.Status);
Assert.Equal(_organization.Id, newOrgUser.OrganizationId);
}
[Fact]
public async Task Import_Existing_Groups_Succeeds()
{
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
var group = await OrganizationTestHelpers.CreateGroup(_factory, _organization.Id);
var request = new OrganizationImportRequestModel();
var addedMember = new OrganizationImportRequestModel.OrganizationImportMemberRequestModel
{
Email = "test@test.com",
ExternalId = "bwtest-externalId",
Deleted = false
};
request.LargeImport = false;
request.OverwriteExisting = false;
request.Groups = [
new OrganizationImportRequestModel.OrganizationImportGroupRequestModel
{
Name = "new-name",
ExternalId = "bwtest-externalId",
MemberExternalIds = []
}
];
request.Members = [addedMember];
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Assert against the database values
var groupRepository = _factory.GetService<IGroupRepository>();
var existingGroups = (await groupRepository.GetManyByOrganizationIdAsync(_organization.Id)).ToArray();
// Assert that we are actually updating the existing group, not adding a new one.
Assert.Single(existingGroups);
Assert.NotNull(existingGroups[0]);
Assert.Equal(group.Id, existingGroups[0].Id);
Assert.Equal("new-name", existingGroups[0].Name);
Assert.Equal(group.ExternalId, existingGroups[0].ExternalId);
var addedOrgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, addedMember.Email);
Assert.NotNull(addedOrgUser);
}
[Fact]
public async Task Import_New_Groups_Succeeds()
{
var group = new Group
{
OrganizationId = _organization.Id,
ExternalId = new Guid().ToString(),
Name = "bwtest1"
};
var request = new OrganizationImportRequestModel();
request.LargeImport = false;
request.OverwriteExisting = false;
request.Groups = [
new OrganizationImportRequestModel.OrganizationImportGroupRequestModel
{
Name = group.Name,
ExternalId = group.ExternalId,
MemberExternalIds = []
}
];
request.Members = [];
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Assert against the database values
var groupRepository = _factory.GetService<IGroupRepository>();
var existingGroups = await groupRepository.GetManyByOrganizationIdAsync(_organization.Id);
var existingGroup = existingGroups.Where(g => g.ExternalId == group.ExternalId).FirstOrDefault();
Assert.NotNull(existingGroup);
Assert.Equal(existingGroup.Name, group.Name);
Assert.Equal(existingGroup.ExternalId, group.ExternalId);
}
[Fact]
public async Task Import_New_And_Existing_Groups_Succeeds()
{
var existingGroup = await OrganizationTestHelpers.CreateGroup(_factory, _organization.Id);
var newGroup = new Group
{
OrganizationId = _organization.Id,
ExternalId = "test",
Name = "bwtest1"
};
var request = new OrganizationImportRequestModel();
request.LargeImport = false;
request.OverwriteExisting = false;
request.Groups = [
new OrganizationImportRequestModel.OrganizationImportGroupRequestModel
{
Name = "new-name",
ExternalId = existingGroup.ExternalId,
MemberExternalIds = []
},
new OrganizationImportRequestModel.OrganizationImportGroupRequestModel
{
Name = newGroup.Name,
ExternalId = newGroup.ExternalId,
MemberExternalIds = []
}
];
request.Members = [];
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Assert against the database values
var groupRepository = _factory.GetService<IGroupRepository>();
var groups = await groupRepository.GetManyByOrganizationIdAsync(_organization.Id);
var newGroupInDb = groups.Where(g => g.ExternalId == newGroup.ExternalId).FirstOrDefault();
Assert.NotNull(newGroupInDb);
Assert.Equal(newGroupInDb.Name, newGroup.Name);
Assert.Equal(newGroupInDb.ExternalId, newGroup.ExternalId);
var existingGroupInDb = groups.Where(g => g.ExternalId == existingGroup.ExternalId).FirstOrDefault();
Assert.NotNull(existingGroupInDb);
Assert.Equal(existingGroup.Id, existingGroupInDb.Id);
Assert.Equal("new-name", existingGroupInDb.Name);
Assert.Equal(existingGroup.ExternalId, existingGroupInDb.ExternalId);
}
[Fact]
public async Task Import_Remove_Member_Without_Master_Password_Throws_400_Error()
{
// ARRANGE: a member without a master password
await OrganizationTestHelpers.CreateUserWithoutMasterPasswordAsync(_factory, Guid.NewGuid() + "@example.com",
_organization.Id);
// ACT: an import request that would remove that member
var request = new OrganizationImportRequestModel
{
LargeImport = false,
OverwriteExisting = true, // removes all members not in the request
Groups = [],
Members = []
};
var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request));
// ASSERT: that a 400 error is thrown with the correct error message
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync();
Assert.Contains("Sync failed. To proceed, disable the 'Remove and re-add users during next sync' setting and try again.", responseContent);
}
}

View File

@@ -1,7 +1,9 @@
using System.Diagnostics;
using Bit.Api.IntegrationTest.Factories;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -60,7 +62,8 @@ public static class OrganizationTestHelpers
OrganizationUserType type,
bool accessSecretsManager = false,
Permissions? permissions = null,
OrganizationUserStatusType userStatusType = OrganizationUserStatusType.Confirmed
OrganizationUserStatusType userStatusType = OrganizationUserStatusType.Confirmed,
string? externalId = null
) where T : class
{
var userRepository = factory.GetService<IUserRepository>();
@@ -76,8 +79,9 @@ public static class OrganizationTestHelpers
Key = null,
Type = type,
Status = userStatusType,
ExternalId = null,
ExternalId = externalId,
AccessSecretsManager = accessSecretsManager,
Email = userEmail
};
if (permissions != null)
@@ -107,7 +111,7 @@ public static class OrganizationTestHelpers
await factory.LoginWithNewAccount(email);
// Create organizationUser
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(factory, organizationId, email, userType,
var organizationUser = await CreateUserAsync(factory, organizationId, email, userType,
permissions: permissions);
return (email, organizationUser);
@@ -130,4 +134,62 @@ public static class OrganizationTestHelpers
await organizationDomainRepository.CreateAsync(verifiedDomain);
}
public static async Task<Group> CreateGroup(ApiApplicationFactory factory, Guid organizationId)
{
var groupRepository = factory.GetService<IGroupRepository>();
var group = new Group
{
OrganizationId = organizationId,
Id = new Guid(),
ExternalId = "bwtest-externalId",
Name = "bwtest"
};
await groupRepository.CreateAsync(group, new List<CollectionAccessSelection>());
return group;
}
/// <summary>
/// Enables the Organization Data Ownership policy for the specified organization.
/// </summary>
public static async Task EnableOrganizationDataOwnershipPolicyAsync<T>(
WebApplicationFactoryBase<T> factory,
Guid organizationId) where T : class
{
var policyRepository = factory.GetService<IPolicyRepository>();
var policy = new Policy
{
OrganizationId = organizationId,
Type = PolicyType.OrganizationDataOwnership,
Enabled = true
};
await policyRepository.CreateAsync(policy);
}
/// <summary>
/// Creates a user account without a Master Password and adds them as a member to the specified organization.
/// </summary>
public static async Task<(User User, OrganizationUser OrganizationUser)> CreateUserWithoutMasterPasswordAsync(ApiApplicationFactory factory, string email, Guid organizationId)
{
var userRepository = factory.GetService<IUserRepository>();
var user = await userRepository.CreateAsync(new User
{
Email = email,
Culture = "en-US",
SecurityStamp = "D7ZH62BWAZ5R5CASKULCDDIQGKDA2EJ6",
PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwMj7W00xS7H0NWasGn7PfEq8VfH3fa5XuZucsKxLLRAHHZk0xGRZJH2lFIznizv3GpF8vzhHhe9VpmMkrdIa5oWhwHpy+D7Z1QCQxuUXzvMKpa95GOntr89nN/mWKpk6abjgjmDcqFJ0lhDqkKnDfes+d8BBd5oEA8p41/Ykz7OfG7AiktVBpTQFW09MQh1NOvcLxVgiUUVRPwNRKrOeCekWDtOjZhASMETv3kI1ogvhHukOQ3ztDzrxvmwnLQ+cXl1EeD8gQnGDp3QLiJqxPgh2EdmANh4IzjRexoDn6BqhRGqLLIoLAbbkoiNrd6NYujrWW0N8KMMoVEXuJL2g4wIDAQAB",
PrivateKey = "2.Ytudv+Qk3ET9hN8whqpuGg==|ijsFhmjaf1aaT9uz+IPhVTzMS+2W/ldAP8LdT5VyJaFdx4HSdLcWSZvz5xWuuW94zfv1Qh+p3iQIuZOr29G4jcx47rYtz4ssiFtB7Ia552ZeF+cb7uuVg40CIe7ycuJQITk00o8gots+wFnaEvk0Vjgycnqutm0jpeBJ1joWJWqTVgSsYdUGLu7PiJywQ9NgY4+bJXqadlcviS3rhPKJXtiXYJhqJqSw+vI0Yxp96MJ0HcFJk/LG22YJPTvL5kzuDq/Wzj40kj8blQ+ag+xHD4P/KJ/MppEB3OpDw3UoJ50Ek+YB9pOqGxZtvqMEzBDsgh0yoz1O992UnhaUqtJ5e9Bxy3PA6cJsdyn9npduNOreEb8vePCidN2XC+chjJpPFpjms9muHLKgfaTIfpiJA2Tz8E9dvSyhHHTE1mY+xEA7P08BYKN3LNoSGIjdiZuouJ1V/KZvCssDfVG1tli2qpnhTIh4m3rAMhbM8WW3B7wCV8N0MpcJJSvndkVcMgRbgWcbivLeXuKdE/K98n01RvOLSJyslhLGCGEQQKw6N3HQ2iELfv84YQZi2fjDK+OqAmXDq1pNcjKX2I8dqBwl31tPC8qSZiWnfinwLdqQTvSQjOIyAHb4sSjAwgdMbCRzUTChRr09l+PAZqGWdMC5N2Bw+bA8WP0l2Wdxuv9Abxl3F7xGeAA9Rw9PU5wGKujaMRmO4V9MFjNyyCcw4D9pzKMW6OUKsHsHE7tsG7KskCzksHzrZGawAt0S41BYQA/JwePCrD3F6dM92anlC1LfA00KJb0tmFdU0yJNmJfR+S78yn8yM6wDgIs2cFB3W1fYfpfUvQm+zzPoEQihNxBxnwFsBtMAOtPy54FjSzKmxsQTrYT9E6NFb8k6ZIIm2gNeOPK9OUJgjw+4g2BXErM6ikHTzM3xcaTq/cQaePZ52emndw1qOtdV06hr2EeuLM8frfLHpsknUe8JeYeW5p9E8QdZjjSN9034usdYNamUdxzmn/Mw/ar8z1xSKS6zcaQoTQ7aYLEX3dWJndc4W64HyiaRkLjO6qLUFeOerfz5UvcxxRY89eAA0KLC2xnGkBMOhXxYzIB3lF8Zxqb4JMhoBGw1n31TDfhRDGDHHEAsZuAIcH7aC5RDVxU08Jxmw4oLmeTDZA5BFcqp2A3fusNVZUnfpmMy6DCJyFprlRl8jSlJMAvhbxVuuLFDZnjl77Z2of796Ur6DgmNwYtMPNEntZPIcZ76VPLWAL8lqiRBm20c4qiwr5rNSr5kry9bR1EfXHwFRjy5pxFQ+5+ilpRl8WPfT/iUuORd8J2wnCmghm7uxiJd9t82kX0s6benhL29dQ1etqt5soX2RnlfKan16GVWoI3xrljIQrCAY4xpdptSpglOnrpSClbN1nhGkDfFPNq2pWhQrDbznDknAJ9MxQaVnLYPhn7I849GMd7EvpSkydwQu7QXn9+H4jxn6UEntNGxcL0xkG+xippvZEe+HBvcDD40efDQW1bDbILLjPb4rNRx4d3xaQnVNaF7L33osm5LgfXAQSwHJiURdkU4zmhtPP4zn0br0OdFlR3mPcrkeNeSvs7FxiKtD6n6s+av+4bKjbLL1OyuwmTnMilL6p+m8ldte0yos/r+zOuxWeI=|euhiXWXehYbFQhlAV6LIECSIPCIRaHbNdr9OI4cTPUM=",
ApiKey = "CfGrD4MoJu3NprOBZNL8tu5ocmtnmU",
KdfIterations = 600000
});
var organizationUser = await CreateUserAsync(factory, organizationId, user.Email,
OrganizationUserType.User, externalId: email);
return (user, organizationUser);
}
}

View File

@@ -8,8 +8,8 @@ using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Installations;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Repositories;
using NSubstitute;
using Xunit;
@@ -166,7 +166,7 @@ public class PushControllerTests
yield return UserTyped(PushType.SyncOrgKeys);
yield return UserTyped(PushType.SyncSettings);
yield return UserTyped(PushType.LogOut);
yield return UserTyped(PushType.PendingSecurityTasks);
yield return UserTyped(PushType.RefreshSecurityTasks);
yield return Typed(new PushSendRequestModel<AuthRequestPushNotification>
{

View File

@@ -0,0 +1,100 @@
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Vault.Models.Response;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.IntegrationTest.Vault.Controllers;
public class SyncControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private readonly IUserRepository _userRepository;
private string _ownerEmail = null!;
public SyncControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
_userRepository = _factory.GetService<IUserRepository>();
}
public async Task InitializeAsync()
{
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
// [BitAutoData]
public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull()
{
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(tempEmail);
await _loginHelper.LoginAsync(tempEmail);
// Remove user's password.
var user = await _userRepository.GetByEmailAsync(tempEmail);
Assert.NotNull(user);
user.MasterPassword = null;
await _userRepository.UpsertAsync(user);
var response = await _client.GetAsync("/sync");
response.EnsureSuccessStatusCode();
var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>();
Assert.NotNull(syncResponseModel);
Assert.NotNull(syncResponseModel.UserDecryption);
Assert.Null(syncResponseModel.UserDecryption.MasterPasswordUnlock);
}
[Theory]
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(KdfType.Argon2id, 11, 128, 5)]
public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull(
KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(tempEmail);
await _loginHelper.LoginAsync(tempEmail);
// Change KDF settings
var user = await _userRepository.GetByEmailAsync(tempEmail);
Assert.NotNull(user);
user.Kdf = kdfType;
user.KdfIterations = kdfIterations;
user.KdfMemory = kdfMemory;
user.KdfParallelism = kdfParallelism;
await _userRepository.UpsertAsync(user);
var response = await _client.GetAsync("/sync");
response.EnsureSuccessStatusCode();
var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>();
Assert.NotNull(syncResponseModel);
Assert.NotNull(syncResponseModel.UserDecryption);
Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock);
Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf);
Assert.Equal(kdfType, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.KdfType);
Assert.Equal(kdfIterations, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Iterations);
Assert.Equal(kdfMemory, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Memory);
Assert.Equal(kdfParallelism, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);
Assert.Equal(user.Key, syncResponseModel.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
Assert.Equal(user.Email.ToLower(), syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt);
}
}

View File

@@ -0,0 +1,125 @@
using System.Security.Claims;
using AutoFixture;
using Bit.Api.AdminConsole.Authorization;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Authorization;
[SutProviderCustomize]
public class OrganizationContextTests
{
[Theory, BitAutoData]
public async Task IsProviderUserForOrganization_UserIsProviderUser_ReturnsTrue(
Guid userId, Guid organizationId, Guid otherOrganizationId,
SutProvider<OrganizationContext> sutProvider)
{
var claimsPrincipal = new ClaimsPrincipal();
var providerUserOrganizations = new List<ProviderUserOrganizationDetails>
{
new() { OrganizationId = organizationId },
new() { OrganizationId = otherOrganizationId }
};
sutProvider.GetDependency<IUserService>()
.GetProperUserId(claimsPrincipal)
.Returns(userId);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed)
.Returns(providerUserOrganizations);
var result = await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);
Assert.True(result);
await sutProvider.GetDependency<IProviderUserRepository>()
.Received(1)
.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed);
}
public static IEnumerable<object[]> UserIsNotProviderUserData()
{
// User has provider organizations, but not for the target organization
yield return
[
new List<ProviderUserOrganizationDetails>
{
new Fixture().Create<ProviderUserOrganizationDetails>()
}
];
// User has no provider organizations
yield return [Array.Empty<ProviderUserOrganizationDetails>()];
}
[Theory, BitMemberAutoData(nameof(UserIsNotProviderUserData))]
public async Task IsProviderUserForOrganization_UserIsNotProviderUser_ReturnsFalse(
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizations,
Guid userId, Guid organizationId,
SutProvider<OrganizationContext> sutProvider)
{
var claimsPrincipal = new ClaimsPrincipal();
sutProvider.GetDependency<IUserService>()
.GetProperUserId(claimsPrincipal)
.Returns(userId);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed)
.Returns(providerUserOrganizations);
var result = await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);
Assert.False(result);
}
[Theory, BitAutoData]
public async Task IsProviderUserForOrganization_UserIdIsNull_ThrowsException(
Guid organizationId,
SutProvider<OrganizationContext> sutProvider)
{
var claimsPrincipal = new ClaimsPrincipal();
sutProvider.GetDependency<IUserService>()
.GetProperUserId(claimsPrincipal)
.Returns((Guid?)null);
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId));
Assert.Equal(OrganizationContext.NoUserIdError, exception.Message);
}
[Theory, BitAutoData]
public async Task IsProviderUserForOrganization_UsesCaching(
Guid userId, Guid organizationId,
SutProvider<OrganizationContext> sutProvider)
{
var claimsPrincipal = new ClaimsPrincipal();
var providerUserOrganizations = new List<ProviderUserOrganizationDetails>
{
new() { OrganizationId = organizationId }
};
sutProvider.GetDependency<IUserService>()
.GetProperUserId(claimsPrincipal)
.Returns(userId);
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed)
.Returns(providerUserOrganizations);
await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);
await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);
await sutProvider.GetDependency<IProviderUserRepository>()
.Received(1)
.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed);
}
}

View File

@@ -0,0 +1,66 @@
using Bit.Api.AdminConsole.Authorization.Requirements;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Core.Test.AdminConsole.Helpers;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Authorization.Requirements;
public class BasePermissionRequirementTests
{
[Theory, BitAutoData]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.Owner)]
public async Task Authorizes_Owners(CurrentContextOrganization organizationClaims)
{
var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));
Assert.True(result);
}
[Theory, BitAutoData]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin)]
public async Task Authorizes_Admins(CurrentContextOrganization organizationClaims)
{
var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));
Assert.True(result);
}
[Theory, BitAutoData]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)]
public async Task Authorizes_Providers(CurrentContextOrganization organizationClaims)
{
var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(true));
Assert.True(result);
}
[Theory, BitAutoData]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)]
public async Task Authorizes_CustomPermission(CurrentContextOrganization organizationClaims)
{
organizationClaims.Permissions.ManageGroups = true;
var result = await new TestCustomPermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));
Assert.True(result);
}
[Theory, BitAutoData]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)]
public async Task DoesNotAuthorize_Users(CurrentContextOrganization organizationClaims)
{
var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));
Assert.False(result);
}
[Theory, BitAutoData]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)]
public async Task DoesNotAuthorize_OtherCustomPermissions(CurrentContextOrganization organizationClaims)
{
organizationClaims.Permissions.ManageGroups = true;
organizationClaims.Permissions = organizationClaims.Permissions.Invert();
var result = await new TestCustomPermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));
Assert.False(result);
}
private class PermissionRequirement() : BasePermissionRequirement(_ => false);
private class TestCustomPermissionRequirement() : BasePermissionRequirement(p => p.ManageGroups);
}

View File

@@ -0,0 +1,84 @@
using Bit.Api.AdminConsole.Authorization.Requirements;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Authorization.Requirements;
[SutProviderCustomize]
public class ManageGroupsOrUsersRequirementTests
{
[Theory]
[CurrentContextOrganizationCustomize]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task AuthorizeAsync_WhenUserTypeCanManageUsers_ThenRequestShouldBeAuthorized(
OrganizationUserType type,
CurrentContextOrganization organization,
SutProvider<ManageGroupsOrUsersRequirement> sutProvider)
{
organization.Type = type;
var actual = await sutProvider.Sut.AuthorizeAsync(organization, () => Task.FromResult(false));
Assert.True(actual);
}
[Theory]
[CurrentContextOrganizationCustomize]
[BitAutoData(OrganizationUserType.Custom, true, false)]
[BitAutoData(OrganizationUserType.Custom, false, true)]
public async Task AuthorizeAsync_WhenCustomUserThatCanManageUsersOrGroups_ThenRequestShouldBeAuthorized(
OrganizationUserType type,
bool canManageUsers,
bool canManageGroups,
CurrentContextOrganization organization,
SutProvider<ManageGroupsOrUsersRequirement> sutProvider)
{
organization.Type = type;
organization.Permissions = new Permissions { ManageUsers = canManageUsers, ManageGroups = canManageGroups };
var actual = await sutProvider.Sut.AuthorizeAsync(organization, () => Task.FromResult(false));
Assert.True(actual);
}
[Theory]
[CurrentContextOrganizationCustomize]
[BitAutoData]
public async Task AuthorizeAsync_WhenProviderUserForAnOrganization_ThenRequestShouldBeAuthorized(
CurrentContextOrganization organization,
SutProvider<ManageGroupsOrUsersRequirement> sutProvider)
{
var actual = await sutProvider.Sut.AuthorizeAsync(organization, IsProviderUserForOrg);
Assert.True(actual);
return;
Task<bool> IsProviderUserForOrg() => Task.FromResult(true);
}
[Theory]
[CurrentContextOrganizationCustomize]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task AuthorizeAsync_WhenUserCannotManageUsersOrGroupsAndIsNotAProviderUser_ThenRequestShouldBeDenied(
OrganizationUserType type,
CurrentContextOrganization organization,
SutProvider<ManageGroupsOrUsersRequirement> sutProvider)
{
organization.Type = type;
organization.Permissions = new Permissions { ManageUsers = false, ManageGroups = false }; // When Type is User, the canManage permissions don't matter
var actual = await sutProvider.Sut.AuthorizeAsync(organization, IsNotProviderUserForOrg);
Assert.False(actual);
return;
Task<bool> IsNotProviderUserForOrg() => Task.FromResult(false);
}
}

View File

@@ -0,0 +1,88 @@
using Bit.Api.AdminConsole.Authorization;
using Bit.Api.AdminConsole.Authorization.Requirements;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Core.Test.AdminConsole.Helpers;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Authorization.Requirements;
public class PermissionRequirementsTests
{
/// <summary>
/// Correlates each IOrganizationRequirement with its custom permission. If you add a new requirement,
/// add a new entry here to have it automatically included in the tests below.
/// </summary>
public static IEnumerable<object[]> RequirementData => new List<object[]>
{
new object[] { new AccessEventLogsRequirement(), nameof(Permissions.AccessEventLogs) },
new object[] { new AccessImportExportRequirement(), nameof(Permissions.AccessImportExport) },
new object[] { new AccessReportsRequirement(), nameof(Permissions.AccessReports) },
new object[] { new ManageAccountRecoveryRequirement(), nameof(Permissions.ManageResetPassword) },
new object[] { new ManageGroupsRequirement(), nameof(Permissions.ManageGroups) },
new object[] { new ManagePoliciesRequirement(), nameof(Permissions.ManagePolicies) },
new object[] { new ManageScimRequirement(), nameof(Permissions.ManageScim) },
new object[] { new ManageSsoRequirement(), nameof(Permissions.ManageSso) },
new object[] { new ManageUsersRequirement(), nameof(Permissions.ManageUsers) },
};
[Theory]
[BitMemberAutoData(nameof(RequirementData))]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)]
public async Task Authorizes_Provider(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization)
{
var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(true));
Assert.True(result);
}
[Theory]
[BitMemberAutoData(nameof(RequirementData))]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.Owner)]
public async Task Authorizes_Owner(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization)
{
var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false));
Assert.True(result);
}
[Theory]
[BitMemberAutoData(nameof(RequirementData))]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin)]
public async Task Authorizes_Admin(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization)
{
var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false));
Assert.True(result);
}
[Theory]
[BitMemberAutoData(nameof(RequirementData))]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)]
public async Task Authorizes_Custom_With_Correct_Permission(IOrganizationRequirement requirement, string permissionName, CurrentContextOrganization organization)
{
organization.Permissions.SetPermission(permissionName, true);
var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false));
Assert.True(result);
}
[Theory]
[BitMemberAutoData(nameof(RequirementData))]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)]
public async Task DoesNotAuthorize_Custom_With_Other_Permissions(IOrganizationRequirement requirement, string permissionName, CurrentContextOrganization organization)
{
organization.Permissions.SetPermission(permissionName, true);
organization.Permissions = organization.Permissions.Invert();
var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false));
Assert.False(result);
}
[Theory]
[BitMemberAutoData(nameof(RequirementData))]
[CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)]
public async Task DoesNotAuthorize_User(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization)
{
var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false));
Assert.False(result);
}
}

View File

@@ -4,14 +4,14 @@ using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Billing.Organizations.Models;
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.OrganizationConnectionConfigs;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;

View File

@@ -28,7 +28,7 @@ public class OrganizationDomainControllerTests
{
sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(false);
var requestAction = async () => await sutProvider.Sut.Get(orgId);
var requestAction = async () => await sutProvider.Sut.GetAll(orgId);
await Assert.ThrowsAsync<UnauthorizedAccessException>(requestAction);
}
@@ -40,7 +40,7 @@ public class OrganizationDomainControllerTests
sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(true);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).ReturnsNull();
var requestAction = async () => await sutProvider.Sut.Get(orgId);
var requestAction = async () => await sutProvider.Sut.GetAll(orgId);
await Assert.ThrowsAsync<NotFoundException>(requestAction);
}
@@ -64,7 +64,7 @@ public class OrganizationDomainControllerTests
}
});
var result = await sutProvider.Sut.Get(orgId);
var result = await sutProvider.Sut.GetAll(orgId);
Assert.IsType<ListResponseModel<OrganizationDomainResponseModel>>(result);
Assert.Equal(orgId, result.Data.Select(x => x.OrganizationId).FirstOrDefault());

View File

@@ -25,6 +25,60 @@ public class OrganizationIntegrationControllerTests
Type = IntegrationType.Webhook
};
[Theory, BitAutoData]
public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAsync(organizationId));
}
[Theory, BitAutoData]
public async Task GetAsync_IntegrationsExist_ReturnsIntegrations(
SutProvider<OrganizationIntegrationController> sutProvider,
Guid organizationId,
List<OrganizationIntegration> integrations)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(organizationId)
.Returns(integrations);
var result = await sutProvider.Sut.GetAsync(organizationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetManyByOrganizationAsync(organizationId);
Assert.Equal(integrations.Count, result.Count);
Assert.All(result, r => Assert.IsType<OrganizationIntegrationResponseModel>(r));
}
[Theory, BitAutoData]
public async Task GetAsync_NoIntegrations_ReturnsEmptyList(
SutProvider<OrganizationIntegrationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(organizationId)
.Returns([]);
var result = await sutProvider.Sut.GetAsync(organizationId);
Assert.Empty(result);
}
[Theory, BitAutoData]
public async Task CreateAsync_Webhook_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationController> sutProvider,

View File

@@ -141,6 +141,131 @@ public class OrganizationIntegrationsConfigurationControllerTests
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty));
}
[Theory, BitAutoData]
public async Task GetAsync_ConfigurationsExist_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
List<OrganizationIntegrationConfiguration> organizationIntegrationConfigurations)
{
organizationIntegration.OrganizationId = organizationId;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfigurations);
var result = await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id);
Assert.NotNull(result);
Assert.Equal(organizationIntegrationConfigurations.Count, result.Count);
Assert.All(result, r => Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(r));
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(organizationIntegration.Id);
}
[Theory, BitAutoData]
public async Task GetAsync_NoConfigurationsExist_ReturnsEmptyList(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
organizationIntegration.OrganizationId = organizationId;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(Arg.Any<Guid>())
.Returns([]);
var result = await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id);
Assert.NotNull(result);
Assert.Empty(result);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(organizationIntegration.Id);
}
// [Theory, BitAutoData]
// public async Task GetAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
// SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
// Guid organizationId,
// OrganizationIntegration organizationIntegration)
// {
// organizationIntegration.OrganizationId = organizationId;
// sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
// sutProvider.GetDependency<ICurrentContext>()
// .OrganizationOwner(organizationId)
// .Returns(true);
// sutProvider.GetDependency<IOrganizationIntegrationRepository>()
// .GetByIdAsync(Arg.Any<Guid>())
// .Returns(organizationIntegration);
// sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
// .GetByIdAsync(Arg.Any<Guid>())
// .ReturnsNull();
//
// await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.Empty, Guid.Empty));
// }
//
[Theory, BitAutoData]
public async Task GetAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.NewGuid()));
}
[Theory, BitAutoData]
public async Task GetAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id));
}
[Theory, BitAutoData]
public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.NewGuid()));
}
[Theory, BitAutoData]
public async Task PostAsync_AllParamsProvided_Slack_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
@@ -189,7 +314,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
@@ -227,7 +352,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"));
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
@@ -390,7 +515,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = null;
@@ -477,7 +602,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
@@ -520,7 +645,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"));
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
@@ -561,7 +686,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;

View File

@@ -29,6 +29,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using NSubstitute;
using Xunit;
@@ -257,7 +258,7 @@ public class OrganizationUsersControllerTests
.GetUsersOrganizationClaimedStatusAsync(organizationUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });
var response = await sutProvider.Sut.Get(organizationUser.Id, false);
var response = await sutProvider.Sut.Get(organizationUser.OrganizationId, organizationUser.Id, false);
Assert.Equal(organizationUser.Id, response.Id);
Assert.True(response.ManagedByOrganization);
@@ -271,7 +272,7 @@ public class OrganizationUsersControllerTests
SutProvider<OrganizationUsersController> sutProvider)
{
GetMany_Setup(organizationAbility, organizationUsers, sutProvider);
var response = await sutProvider.Sut.Get(organizationAbility.Id, false, false);
var response = await sutProvider.Sut.GetAll(organizationAbility.Id, false, false);
Assert.True(response.Data.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));
}
@@ -305,84 +306,14 @@ public class OrganizationUsersControllerTests
[Theory]
[BitAutoData]
public async Task GetAccountRecoveryDetails_WithoutManageResetPasswordPermission_Throws(
Guid organizationId,
OrganizationUserBulkRequestModel bulkRequestModel,
SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageResetPassword(organizationId).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAccountRecoveryDetails(organizationId, bulkRequestModel));
}
[Theory]
[BitAutoData]
public async Task DeleteAccount_WhenUserCanManageUsers_Success(
Guid orgId, Guid id, User currentUser, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
await sutProvider.Sut.DeleteAccount(orgId, id);
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountCommand>()
.Received(1)
.DeleteUserAsync(orgId, id, currentUser.Id);
}
[Theory]
[BitAutoData]
public async Task DeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException(
public async Task DeleteAccount_WhenCurrentUserNotFound_ReturnsUnauthorizedResult(
Guid orgId, Guid id, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(false);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs((Guid?)null);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.DeleteAccount(orgId, id));
}
var result = await sutProvider.Sut.DeleteAccount(orgId, id);
[Theory]
[BitAutoData]
public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
Guid orgId, Guid id, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
sutProvider.Sut.DeleteAccount(orgId, id));
}
[Theory]
[BitAutoData]
public async Task BulkDeleteAccount_WhenUserCanManageUsers_Success(
Guid orgId, OrganizationUserBulkRequestModel model, User currentUser,
List<(Guid, string)> deleteResults, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountCommand>()
.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id)
.Returns(deleteResults);
var response = await sutProvider.Sut.BulkDeleteAccount(orgId, model);
Assert.Equal(deleteResults.Count, response.Data.Count());
Assert.True(response.Data.All(r => deleteResults.Any(res => res.Item1 == r.Id && res.Item2 == r.Error)));
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountCommand>()
.Received(1)
.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
}
[Theory]
[BitAutoData]
public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException(
Guid orgId, OrganizationUserBulkRequestModel model, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.BulkDeleteAccount(orgId, model));
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory]

View File

@@ -2,10 +2,12 @@
using AutoFixture.Xunit2;
using Bit.Api.AdminConsole.Controllers;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Models.Request.Organizations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
@@ -29,6 +31,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using NSubstitute;
using Xunit;
@@ -293,4 +296,40 @@ public class OrganizationsControllerTests : IDisposable
Assert.True(result.ResetPasswordEnabled);
}
[Theory, AutoData]
public async Task PutCollectionManagement_ValidRequest_Success(
Organization organization,
OrganizationCollectionManagementUpdateRequestModel model)
{
// Arrange
_currentContext.OrganizationOwner(organization.Id).Returns(true);
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
_pricingClient.GetPlan(Arg.Any<PlanType>()).Returns(plan);
_organizationService
.UpdateCollectionManagementSettingsAsync(
organization.Id,
Arg.Is<OrganizationCollectionManagementSettings>(s =>
s.LimitCollectionCreation == model.LimitCollectionCreation &&
s.LimitCollectionDeletion == model.LimitCollectionDeletion &&
s.LimitItemDeletion == model.LimitItemDeletion &&
s.AllowAdminAccessToAllCollectionItems == model.AllowAdminAccessToAllCollectionItems))
.Returns(organization);
// Act
await _sut.PutCollectionManagement(organization.Id, model);
// Assert
await _organizationService
.Received(1)
.UpdateCollectionManagementSettingsAsync(
organization.Id,
Arg.Is<OrganizationCollectionManagementSettings>(s =>
s.LimitCollectionCreation == model.LimitCollectionCreation &&
s.LimitCollectionDeletion == model.LimitCollectionDeletion &&
s.LimitItemDeletion == model.LimitItemDeletion &&
s.AllowAdminAccessToAllCollectionItems == model.AllowAdminAccessToAllCollectionItems));
}
}

View File

@@ -1,12 +1,18 @@
using Bit.Api.AdminConsole.Controllers;
#nullable enable
using Bit.Api.AdminConsole.Controllers;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
@@ -16,98 +22,312 @@ namespace Bit.Api.Test.AdminConsole.Controllers;
[SutProviderCustomize]
public class SlackIntegrationControllerTests
{
private const string _slackToken = "xoxb-test-token";
private const string _validSlackCode = "A_test_code";
[Theory, BitAutoData]
public async Task CreateAsync_AllParamsProvided_Succeeds(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
public async Task CreateAsync_AllParamsProvided_Succeeds(
SutProvider<SlackIntegrationController> sutProvider,
OrganizationIntegration integration)
{
var token = "xoxb-test-token";
integration.Type = IntegrationType.Slack;
integration.Configuration = null;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns("https://localhost");
sutProvider.GetDependency<ISlackService>()
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
.Returns(token);
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
.Returns(_slackToken);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegration>())
.Returns(callInfo => callInfo.Arg<OrganizationIntegration>());
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, "A_test_code");
.GetByIdAsync(integration.Id)
.Returns(integration);
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
var requestAction = await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString());
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegration>());
.UpsertAsync(Arg.Any<OrganizationIntegration>());
Assert.IsType<CreatedResult>(requestAction);
}
[Theory, BitAutoData]
public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(
SutProvider<SlackIntegrationController> sutProvider,
OrganizationIntegration integration)
{
integration.Type = IntegrationType.Slack;
integration.Configuration = null;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns("https://localhost");
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integration.Id)
.Returns(integration);
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(organizationId, string.Empty));
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.CreateAsync(string.Empty, state.ToString()));
}
[Theory, BitAutoData]
public async Task CreateAsync_SlackServiceReturnsEmpty_ThrowsBadRequest(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
public async Task CreateAsync_SlackServiceReturnsEmpty_ThrowsBadRequest(
SutProvider<SlackIntegrationController> sutProvider,
OrganizationIntegration integration)
{
integration.Type = IntegrationType.Slack;
integration.Configuration = null;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns("https://localhost");
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integration.Id)
.Returns(integration);
sutProvider.GetDependency<ISlackService>()
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
.Returns(string.Empty);
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(organizationId, "A_test_code"));
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
}
[Theory, BitAutoData]
public async Task CreateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
public async Task CreateAsync_StateEmpty_ThrowsNotFound(
SutProvider<SlackIntegrationController> sutProvider)
{
var token = "xoxb-test-token";
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns("https://localhost");
sutProvider.GetDependency<ISlackService>()
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
.Returns(token);
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
.Returns(_slackToken);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(organizationId, "A_test_code"));
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, String.Empty));
}
[Theory, BitAutoData]
public async Task RedirectAsync_Success(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
public async Task CreateAsync_StateExpired_ThrowsNotFound(
SutProvider<SlackIntegrationController> sutProvider,
OrganizationIntegration integration)
{
var expectedUrl = $"https://localhost/{organizationId}";
var timeProvider = new FakeTimeProvider(new DateTime(2024, 4, 3, 2, 1, 0, DateTimeKind.Utc));
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns("https://localhost");
sutProvider.GetDependency<ISlackService>()
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
.Returns(_slackToken);
var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);
timeProvider.Advance(TimeSpan.FromMinutes(30));
sutProvider.SetDependency<TimeProvider>(timeProvider);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
}
[Theory, BitAutoData]
public async Task CreateAsync_StateHasNonexistentIntegration_ThrowsNotFound(
SutProvider<SlackIntegrationController> sutProvider,
OrganizationIntegration integration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns("https://localhost");
sutProvider.GetDependency<ISlackService>()
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
.Returns(_slackToken);
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
}
[Theory, BitAutoData]
public async Task CreateAsync_StateHasWrongOgranizationHash_ThrowsNotFound(
SutProvider<SlackIntegrationController> sutProvider,
OrganizationIntegration integration,
OrganizationIntegration wrongOrgIntegration)
{
wrongOrgIntegration.Id = integration.Id;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(expectedUrl);
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns("https://localhost");
sutProvider.GetDependency<ISlackService>()
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
.Returns(_slackToken);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integration.Id)
.Returns(wrongOrgIntegration);
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
}
[Theory, BitAutoData]
public async Task CreateAsync_StateHasNonEmptyIntegration_ThrowsNotFound(
SutProvider<SlackIntegrationController> sutProvider,
OrganizationIntegration integration)
{
integration.Type = IntegrationType.Slack;
integration.Configuration = "{}";
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns("https://localhost");
sutProvider.GetDependency<ISlackService>()
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
.Returns(_slackToken);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integration.Id)
.Returns(integration);
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
}
[Theory, BitAutoData]
public async Task CreateAsync_StateHasNonSlackIntegration_ThrowsNotFound(
SutProvider<SlackIntegrationController> sutProvider,
OrganizationIntegration integration)
{
integration.Type = IntegrationType.Hec;
integration.Configuration = null;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns("https://localhost");
sutProvider.GetDependency<ISlackService>()
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
.Returns(_slackToken);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integration.Id)
.Returns(integration);
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
}
[Theory, BitAutoData]
public async Task RedirectAsync_Success(
SutProvider<SlackIntegrationController> sutProvider,
OrganizationIntegration integration)
{
integration.Configuration = null;
var expectedUrl = "https://localhost/";
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns(expectedUrl);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(integration.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(integration.OrganizationId)
.Returns([]);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegration>())
.Returns(integration);
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
var requestAction = await sutProvider.Sut.RedirectAsync(integration.OrganizationId);
Assert.IsType<RedirectResult>(requestAction);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegration>());
sutProvider.GetDependency<ISlackService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());
}
[Theory, BitAutoData]
public async Task RedirectAsync_IntegrationAlreadyExistsWithNullConfig_Success(
SutProvider<SlackIntegrationController> sutProvider,
Guid organizationId,
OrganizationIntegration integration)
{
integration.OrganizationId = organizationId;
integration.Configuration = null;
integration.Type = IntegrationType.Slack;
var expectedUrl = "https://localhost/";
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns(expectedUrl);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.HttpContext.Request.Scheme
.Returns("https");
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(organizationId)
.Returns([integration]);
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
var requestAction = await sutProvider.Sut.RedirectAsync(organizationId);
var redirectResult = Assert.IsType<RedirectResult>(requestAction);
Assert.Equal(expectedUrl, redirectResult.Url);
var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
Assert.IsType<RedirectResult>(requestAction);
sutProvider.GetDependency<ISlackService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());
}
[Theory, BitAutoData]
public async Task RedirectAsync_SlackServiceReturnsEmpty_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest(
SutProvider<SlackIntegrationController> sutProvider,
Guid organizationId,
OrganizationIntegration integration)
{
integration.OrganizationId = organizationId;
integration.Configuration = "{}";
integration.Type = IntegrationType.Slack;
var expectedUrl = "https://localhost/";
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(string.Empty);
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns(expectedUrl);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(organizationId)
.Returns([integration]);
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
}
[Theory, BitAutoData]
public async Task RedirectAsync_SlackServiceReturnsEmpty_ThrowsNotFound(
SutProvider<SlackIntegrationController> sutProvider,
Guid organizationId,
OrganizationIntegration integration)
{
integration.OrganizationId = organizationId;
integration.Configuration = null;
var expectedUrl = "https://localhost/";
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync)))
.Returns(expectedUrl);
sutProvider.GetDependency<ICurrentContext>()
.HttpContext.Request.Scheme
.Returns("https");
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(organizationId)
.Returns([]);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegration>())
.Returns(integration);
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(string.Empty);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
}
@@ -116,14 +336,9 @@ public class SlackIntegrationControllerTests
public async Task RedirectAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(string.Empty);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
sutProvider.GetDependency<ICurrentContext>()
.HttpContext.Request.Scheme
.Returns("https");
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
}

View File

@@ -0,0 +1,60 @@
using Bit.Api.AdminConsole.Jobs;
using Bit.Core;
using Bit.Core.AdminConsole.Models.Data.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Quartz;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Jobs;
[SutProviderCustomize]
public class OrganizationSubscriptionUpdateJobTests
{
[Theory]
[BitAutoData]
public async Task ExecuteJobAsync_WhenScimInviteUserIsDisabled_ThenQueryAndCommandAreNotExecuted(
SutProvider<OrganizationSubscriptionUpdateJob> sutProvider)
{
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization)
.Returns(false);
var contextMock = Substitute.For<IJobExecutionContext>();
await sutProvider.Sut.Execute(contextMock);
await sutProvider.GetDependency<IGetOrganizationSubscriptionsToUpdateQuery>()
.DidNotReceive()
.GetOrganizationSubscriptionsToUpdateAsync();
await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()
.DidNotReceive()
.UpdateOrganizationSubscriptionAsync(Arg.Any<IEnumerable<OrganizationSubscriptionUpdate>>());
}
[Theory]
[BitAutoData]
public async Task ExecuteJobAsync_WhenScimInviteUserIsEnabled_ThenQueryAndCommandAreExecuted(
SutProvider<OrganizationSubscriptionUpdateJob> sutProvider)
{
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization)
.Returns(true);
var contextMock = Substitute.For<IJobExecutionContext>();
await sutProvider.Sut.Execute(contextMock);
await sutProvider.GetDependency<IGetOrganizationSubscriptionsToUpdateQuery>()
.Received(1)
.GetOrganizationSubscriptionsToUpdateAsync();
await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()
.Received(1)
.UpdateOrganizationSubscriptionAsync(Arg.Any<IEnumerable<OrganizationSubscriptionUpdate>>());
}
}

View File

@@ -17,13 +17,13 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Template = "template"
};
Assert.False(model.IsValidForType(IntegrationType.CloudBillingSync));
Assert.False(condition: model.IsValidForType(IntegrationType.CloudBillingSync));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData(data: null)]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyConfiguration_ReturnsFalse(string? config)
{
var model = new OrganizationIntegrationConfigurationRequestModel
@@ -32,25 +32,81 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Template = "template"
};
var result = model.IsValidForType(IntegrationType.Slack);
Assert.False(result);
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyNonNullHecConfiguration_ReturnsFalse(string? config)
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
}
[Fact]
public void IsValidForType_NullHecConfiguration_ReturnsTrue()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = null,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Hec));
}
[Theory]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyNonNullDatadogConfiguration_ReturnsFalse(string? config)
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.Datadog));
}
[Fact]
public void IsValidForType_NullDatadogConfiguration_ReturnsTrue()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = null,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Datadog));
}
[Theory]
[InlineData(data: null)]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyTemplate_ReturnsFalse(string? template)
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN"));
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = template
};
Assert.False(model.IsValidForType(IntegrationType.Webhook));
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
}
[Fact]
@@ -62,14 +118,16 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Template = "template"
};
Assert.False(model.IsValidForType(IntegrationType.Webhook));
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
}
[Fact]
public void IsValidForType_InvalidJsonFilters_ReturnsFalse()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com"));
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://example.com")));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
@@ -89,13 +147,13 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Template = "template"
};
Assert.False(model.IsValidForType(IntegrationType.Scim));
Assert.False(condition: model.IsValidForType(IntegrationType.Scim));
}
[Fact]
public void IsValidForType_ValidSlackConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345"));
var config = JsonSerializer.Serialize(value: new SlackIntegrationConfiguration(ChannelId: "C12345"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
@@ -103,7 +161,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Slack));
Assert.True(condition: model.IsValidForType(IntegrationType.Slack));
}
[Fact]
@@ -136,33 +194,39 @@ public class OrganizationIntegrationConfigurationRequestModelTests
[Fact]
public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com"));
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost")));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Webhook));
Assert.True(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN"));
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Webhook));
Assert.True(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_ValidWebhookConfigurationWithFilters_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN"));
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(
Uri: new Uri("https://example.com"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
@@ -197,6 +261,6 @@ public class OrganizationIntegrationConfigurationRequestModelTests
var unknownType = (IntegrationType)999;
Assert.False(model.IsValidForType(unknownType));
Assert.False(condition: model.IsValidForType(unknownType));
}
}

View File

@@ -1,5 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
using Xunit;
@@ -70,7 +72,7 @@ public class OrganizationIntegrationRequestModelTests
}
[Fact]
public void Validate_Webhook_WithConfiguration_ReturnsConfigurationError()
public void Validate_Webhook_WithInvalidConfiguration_ReturnsConfigurationError()
{
var model = new OrganizationIntegrationRequestModel
{
@@ -82,7 +84,115 @@ public class OrganizationIntegrationRequestModelTests
Assert.Single(results);
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
Assert.Contains("must not include configuration", results[0].ErrorMessage);
Assert.Contains("Must include valid", results[0].ErrorMessage);
}
[Fact]
public void Validate_Webhook_WithValidConfiguration_ReturnsNoErrors()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Webhook,
Configuration = JsonSerializer.Serialize(new WebhookIntegration(new Uri("https://example.com")))
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Empty(results);
}
[Fact]
public void Validate_Hec_WithNullConfiguration_ReturnsError()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Hec,
Configuration = null
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Single(results);
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
Assert.Contains("Must include valid", results[0].ErrorMessage);
}
[Fact]
public void Validate_Hec_WithInvalidConfiguration_ReturnsError()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Hec,
Configuration = "Not valid"
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Single(results);
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
Assert.Contains("Must include valid", results[0].ErrorMessage);
}
[Fact]
public void Validate_Hec_WithValidConfiguration_ReturnsNoErrors()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Hec,
Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri("http://localhost"), Scheme: "Bearer", Token: "Token"))
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Empty(results);
}
[Fact]
public void Validate_Datadog_WithNullConfiguration_ReturnsError()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Datadog,
Configuration = null
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Single(results);
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
Assert.Contains("Must include valid", results[0].ErrorMessage);
}
[Fact]
public void Validate_Datadog_WithInvalidConfiguration_ReturnsError()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Datadog,
Configuration = "Not valid"
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Single(results);
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
Assert.Contains("Must include valid", results[0].ErrorMessage);
}
[Fact]
public void Validate_Datadog_WithValidConfiguration_ReturnsNoErrors()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Datadog,
Configuration = JsonSerializer.Serialize(
new DatadogIntegration(ApiKey: "API1234", Uri: new Uri("http://localhost"))
)
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Empty(results);
}
[Fact]

View File

@@ -0,0 +1,303 @@

using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Context;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Models.Request;
[SutProviderCustomize]
public class SavePolicyRequestTests
{
[Theory, BitAutoData]
public async Task ToSavePolicyModelAsync_WithValidData_ReturnsCorrectSavePolicyModel(
Guid organizationId,
Guid userId)
{
// Arrange
var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var testData = new Dictionary<string, object> { { "test", "value" } };
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.TwoFactorAuthentication,
Enabled = true,
Data = testData
},
Metadata = new Dictionary<string, object>()
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
// Assert
Assert.Equal(PolicyType.TwoFactorAuthentication, result.PolicyUpdate.Type);
Assert.Equal(organizationId, result.PolicyUpdate.OrganizationId);
Assert.True(result.PolicyUpdate.Enabled);
Assert.NotNull(result.PolicyUpdate.Data);
var deserializedData = JsonSerializer.Deserialize<Dictionary<string, object>>(result.PolicyUpdate.Data);
Assert.Equal("value", deserializedData["test"].ToString());
Assert.Equal(userId, result!.PerformedBy.UserId);
Assert.True(result!.PerformedBy.IsOrganizationOwnerOrProvider);
Assert.IsType<EmptyMetadataModel>(result.Metadata);
}
[Theory, BitAutoData]
public async Task ToSavePolicyModelAsync_WithNullData_HandlesCorrectly(
Guid organizationId,
Guid userId)
{
// Arrange
var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(false);
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.SingleOrg,
Enabled = false,
Data = null
},
Metadata = null
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
// Assert
Assert.Null(result.PolicyUpdate.Data);
Assert.False(result.PolicyUpdate.Enabled);
Assert.Equal(userId, result!.PerformedBy.UserId);
Assert.False(result!.PerformedBy.IsOrganizationOwnerOrProvider);
}
[Theory, BitAutoData]
public async Task ToSavePolicyModelAsync_WithNonOrganizationOwner_HandlesCorrectly(
Guid organizationId,
Guid userId)
{
// Arrange
var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.SingleOrg,
Enabled = false,
Data = null
},
Metadata = null
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
// Assert
Assert.Null(result.PolicyUpdate.Data);
Assert.False(result.PolicyUpdate.Enabled);
Assert.Equal(userId, result!.PerformedBy.UserId);
Assert.True(result!.PerformedBy.IsOrganizationOwnerOrProvider);
}
[Theory, BitAutoData]
public async Task ToSavePolicyModelAsync_OrganizationDataOwnership_WithValidMetadata_ReturnsCorrectMetadata(
Guid organizationId,
Guid userId,
string defaultCollectionName)
{
// Arrange
var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.OrganizationDataOwnership,
Enabled = true,
Data = null
},
Metadata = new Dictionary<string, object>
{
{ "defaultUserCollectionName", defaultCollectionName }
}
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
// Assert
Assert.IsType<OrganizationModelOwnershipPolicyModel>(result.Metadata);
var metadata = (OrganizationModelOwnershipPolicyModel)result.Metadata;
Assert.Equal(defaultCollectionName, metadata.DefaultUserCollectionName);
}
[Theory, BitAutoData]
public async Task ToSavePolicyModelAsync_OrganizationDataOwnership_WithNullMetadata_ReturnsEmptyMetadata(
Guid organizationId,
Guid userId)
{
// Arrange
var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.OrganizationDataOwnership,
Enabled = true,
Data = null
},
Metadata = null
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
// Assert
Assert.NotNull(result);
Assert.IsType<EmptyMetadataModel>(result.Metadata);
}
private static readonly Dictionary<string, object> _complexData = new Dictionary<string,
object>
{
{ "stringValue", "test" },
{ "numberValue", 42 },
{ "boolValue", true },
{ "arrayValue", new[] { "item1", "item2" } },
{ "nestedObject", new Dictionary<string, object> { { "nested", "value" } } }
};
[Theory, BitAutoData]
public async Task ToSavePolicyModelAsync_ComplexData_SerializesCorrectly(
Guid organizationId,
Guid userId)
{
// Arrange
var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.ResetPassword,
Enabled = true,
Data = _complexData
},
Metadata = new Dictionary<string, object>()
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
// Assert
var deserializedData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(result.PolicyUpdate.Data);
Assert.Equal("test", deserializedData["stringValue"].GetString());
Assert.Equal(42, deserializedData["numberValue"].GetInt32());
Assert.True(deserializedData["boolValue"].GetBoolean());
Assert.Equal(2, deserializedData["arrayValue"].GetArrayLength());
var array = deserializedData["arrayValue"].EnumerateArray()
.Select(e => e.GetString())
.ToArray();
Assert.Contains("item1", array);
Assert.Contains("item2", array);
Assert.True(deserializedData["nestedObject"].TryGetProperty("nested", out var nestedValue));
Assert.Equal("value", nestedValue.GetString());
}
[Theory, BitAutoData]
public async Task MapToPolicyMetadata_UnknownPolicyType_ReturnsEmptyMetadata(
Guid organizationId,
Guid userId)
{
// Arrange
var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.MaximumVaultTimeout,
Enabled = true,
Data = null
},
Metadata = new Dictionary<string, object>
{
{ "someProperty", "someValue" }
}
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
// Assert
Assert.NotNull(result);
Assert.IsType<EmptyMetadataModel>(result.Metadata);
}
[Theory, BitAutoData]
public async Task MapToPolicyMetadata_JsonSerializationException_ReturnsEmptyMetadata(
Guid organizationId,
Guid userId)
{
// Arrange
var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var errorDictionary = BuildErrorDictionary();
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.OrganizationDataOwnership,
Enabled = true,
Data = null
},
Metadata = errorDictionary
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
// Assert
Assert.NotNull(result);
Assert.IsType<EmptyMetadataModel>(result.Metadata);
}
private static Dictionary<string, object> BuildErrorDictionary()
{
var circularDict = new Dictionary<string, object>();
circularDict["self"] = circularDict;
return circularDict;
}
}

View File

@@ -0,0 +1,117 @@
#nullable enable
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Models.Response.Organizations;
public class OrganizationIntegrationResponseModelTests
{
[Theory, BitAutoData]
public void Status_CloudBillingSync_AlwaysNotApplicable(OrganizationIntegration oi)
{
oi.Type = IntegrationType.CloudBillingSync;
oi.Configuration = null;
var model = new OrganizationIntegrationResponseModel(oi);
Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);
model.Configuration = "{}";
Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);
}
[Theory, BitAutoData]
public void Status_Scim_AlwaysNotApplicable(OrganizationIntegration oi)
{
oi.Type = IntegrationType.Scim;
oi.Configuration = null;
var model = new OrganizationIntegrationResponseModel(oi);
Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);
model.Configuration = "{}";
Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);
}
[Theory, BitAutoData]
public void Status_Slack_NullConfig_ReturnsInitiated(OrganizationIntegration oi)
{
oi.Type = IntegrationType.Slack;
oi.Configuration = null;
var model = new OrganizationIntegrationResponseModel(oi);
Assert.Equal(OrganizationIntegrationStatus.Initiated, model.Status);
}
[Theory, BitAutoData]
public void Status_Slack_WithConfig_ReturnsCompleted(OrganizationIntegration oi)
{
oi.Type = IntegrationType.Slack;
oi.Configuration = "{}";
var model = new OrganizationIntegrationResponseModel(oi);
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
}
[Theory, BitAutoData]
public void Status_Webhook_AlwaysCompleted(OrganizationIntegration oi)
{
oi.Type = IntegrationType.Webhook;
oi.Configuration = null;
var model = new OrganizationIntegrationResponseModel(oi);
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
model.Configuration = "{}";
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
}
[Theory, BitAutoData]
public void Status_Hec_NullConfig_ReturnsInvalid(OrganizationIntegration oi)
{
oi.Type = IntegrationType.Hec;
oi.Configuration = null;
var model = new OrganizationIntegrationResponseModel(oi);
Assert.Equal(OrganizationIntegrationStatus.Invalid, model.Status);
}
[Theory, BitAutoData]
public void Status_Hec_WithConfig_ReturnsCompleted(OrganizationIntegration oi)
{
oi.Type = IntegrationType.Hec;
oi.Configuration = "{}";
var model = new OrganizationIntegrationResponseModel(oi);
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
}
[Theory, BitAutoData]
public void Status_Datadog_NullConfig_ReturnsInvalid(OrganizationIntegration oi)
{
oi.Type = IntegrationType.Datadog;
oi.Configuration = null;
var model = new OrganizationIntegrationResponseModel(oi);
Assert.Equal(OrganizationIntegrationStatus.Invalid, model.Status);
}
[Theory, BitAutoData]
public void Status_Datadog_WithConfig_ReturnsCompleted(OrganizationIntegration oi)
{
oi.Type = IntegrationType.Datadog;
oi.Configuration = "{}";
var model = new OrganizationIntegrationResponseModel(oi);
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
}
}

View File

@@ -25,9 +25,4 @@
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Core.Test\Core.Test.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Auth\" />
<Folder Include="Auth\Controllers\" />
</ItemGroup>
</Project>

View File

@@ -4,11 +4,13 @@ using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -31,6 +33,9 @@ public class AccountsControllerTests : IDisposable
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IFeatureService _featureService;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand;
public AccountsControllerTests()
{
@@ -43,6 +48,8 @@ public class AccountsControllerTests : IDisposable
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
_featureService = Substitute.For<IFeatureService>();
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
_sut = new AccountsController(
_organizationService,
@@ -53,7 +60,9 @@ public class AccountsControllerTests : IDisposable
_setInitialMasterPasswordCommand,
_tdeOffboardingPasswordCommand,
_twoFactorIsEnabledQuery,
_featureService
_featureService,
_twoFactorEmailService,
_changeKdfCommand
);
}
@@ -236,12 +245,18 @@ public class AccountsControllerTests : IDisposable
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangePasswordAsync(user, default, default, default, default)
_userService.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Success));
await _sut.PostPassword(new PasswordRequestModel());
await _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
});
await _userService.Received(1).ChangePasswordAsync(user, default, default, default, default);
await _userService.Received(1).ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
}
[Fact]
@@ -250,7 +265,13 @@ public class AccountsControllerTests : IDisposable
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostPassword(new PasswordRequestModel())
() => _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
})
);
}
@@ -259,11 +280,17 @@ public class AccountsControllerTests : IDisposable
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangePasswordAsync(user, default, default, default, default)
_userService.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Failed()));
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostPassword(new PasswordRequestModel())
() => _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
})
);
}
@@ -547,6 +574,70 @@ public class AccountsControllerTests : IDisposable
Assert.Equal(model.VerifyDevices, user.VerifyDevices);
}
[Theory]
[BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenUserNotFound_ShouldFail(
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.ResendNewDeviceOtpAsync(model));
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenSecretNotValid_ShouldFail(
User user,
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(false));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.ResendNewDeviceOtpAsync(model));
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenTokenValid_SendsEmail(User user,
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(true));
// Act
await _sut.ResendNewDeviceOtpAsync(model);
// Assert
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user);
}
[Theory]
[BitAutoData]
public async Task PostKdf_WithNullAuthenticationData_ShouldFail(
User user, PasswordRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
model.AuthenticationData = null;
// Act
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
}
[Theory]
[BitAutoData]
public async Task PostKdf_WithNullUnlockData_ShouldFail(
User user, PasswordRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
model.UnlockData = null;
// Act
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
}
// Below are helper functions that currently belong to this
// test class, but ultimately may need to be split out into
// something greater in order to share common test steps with

View File

@@ -0,0 +1,370 @@
using System.Security.Claims;
using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Response;
using Bit.Api.Models.Response;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Services;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Auth.Controllers;
[ControllerCustomize(typeof(AuthRequestsController))]
[SutProviderCustomize]
public class AuthRequestsControllerTests
{
const string _testGlobalSettingsBaseUri = "https://vault.test.dev";
[Theory, BitAutoData]
public async Task Get_ReturnsExpectedResult(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequest authRequest)
{
// Arrange
SetBaseServiceUri(sutProvider);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
sutProvider.GetDependency<IAuthRequestRepository>()
.GetManyByUserIdAsync(user.Id)
.Returns([authRequest]);
// Act
var result = await sutProvider.Sut.GetAll();
// Assert
Assert.NotNull(result);
var expectedCount = 1;
Assert.Equal(result.Data.Count(), expectedCount);
Assert.IsType<ListResponseModel<AuthRequestResponseModel>>(result);
}
[Theory, BitAutoData]
public async Task GetById_ThrowsNotFoundException(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequest authRequest)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
sutProvider.GetDependency<IAuthRequestService>()
.GetAuthRequestAsync(authRequest.Id, user.Id)
.Returns((AuthRequest)null);
// Act
// Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.Get(authRequest.Id));
}
[Theory, BitAutoData]
public async Task GetById_ReturnsAuthRequest(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequest authRequest)
{
// Arrange
SetBaseServiceUri(sutProvider);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
sutProvider.GetDependency<IAuthRequestService>()
.GetAuthRequestAsync(authRequest.Id, user.Id)
.Returns(authRequest);
// Act
var result = await sutProvider.Sut.Get(authRequest.Id);
// Assert
Assert.NotNull(result);
Assert.IsType<AuthRequestResponseModel>(result);
}
[Theory, BitAutoData]
public async Task GetPending_ReturnsExpectedResult(
SutProvider<AuthRequestsController> sutProvider,
User user,
PendingAuthRequestDetails authRequest)
{
// Arrange
SetBaseServiceUri(sutProvider);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
sutProvider.GetDependency<IAuthRequestRepository>()
.GetManyPendingAuthRequestByUserId(user.Id)
.Returns([authRequest]);
// Act
var result = await sutProvider.Sut.GetPendingAuthRequestsAsync();
// Assert
Assert.NotNull(result);
var expectedCount = 1;
Assert.Equal(result.Data.Count(), expectedCount);
Assert.IsType<ListResponseModel<PendingAuthRequestResponseModel>>(result);
}
[Theory, BitAutoData]
public async Task GetResponseById_ThrowsNotFoundException(
SutProvider<AuthRequestsController> sutProvider,
AuthRequest authRequest)
{
// Arrange
sutProvider.GetDependency<IAuthRequestService>()
.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode)
.Returns((AuthRequest)null);
// Act
// Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.GetResponse(authRequest.Id, authRequest.AccessCode));
}
[Theory, BitAutoData]
public async Task GetResponseById_ReturnsAuthRequest(
SutProvider<AuthRequestsController> sutProvider,
AuthRequest authRequest)
{
// Arrange
SetBaseServiceUri(sutProvider);
sutProvider.GetDependency<IAuthRequestService>()
.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode)
.Returns(authRequest);
// Act
var result = await sutProvider.Sut.GetResponse(authRequest.Id, authRequest.AccessCode);
// Assert
Assert.NotNull(result);
Assert.IsType<AuthRequestResponseModel>(result);
}
[Theory, BitAutoData]
public async Task Post_AdminApprovalRequest_ThrowsBadRequestException(
SutProvider<AuthRequestsController> sutProvider,
AuthRequestCreateRequestModel authRequest)
{
// Arrange
authRequest.Type = AuthRequestType.AdminApproval;
// Act
// Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.Post(authRequest));
var expectedMessage = "You must be authenticated to create a request of that type.";
Assert.Equal(exception.Message, expectedMessage);
}
[Theory, BitAutoData]
public async Task Post_ReturnsAuthRequest(
SutProvider<AuthRequestsController> sutProvider,
AuthRequestCreateRequestModel requestModel,
AuthRequest authRequest)
{
// Arrange
SetBaseServiceUri(sutProvider);
requestModel.Type = AuthRequestType.AuthenticateAndUnlock;
sutProvider.GetDependency<IAuthRequestService>()
.CreateAuthRequestAsync(requestModel)
.Returns(authRequest);
// Act
var result = await sutProvider.Sut.Post(requestModel);
// Assert
Assert.NotNull(result);
Assert.IsType<AuthRequestResponseModel>(result);
}
[Theory, BitAutoData]
public async Task PostAdminRequest_ReturnsAuthRequest(
SutProvider<AuthRequestsController> sutProvider,
AuthRequestCreateRequestModel requestModel,
AuthRequest authRequest)
{
// Arrange
SetBaseServiceUri(sutProvider);
requestModel.Type = AuthRequestType.AuthenticateAndUnlock;
sutProvider.GetDependency<IAuthRequestService>()
.CreateAuthRequestAsync(requestModel)
.Returns(authRequest);
// Act
var result = await sutProvider.Sut.PostAdminRequest(requestModel);
// Assert
Assert.NotNull(result);
Assert.IsType<AuthRequestResponseModel>(result);
}
[Theory, BitAutoData]
public async Task Put_WithRequestNotApproved_ReturnsAuthRequest(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequestUpdateRequestModel requestModel,
AuthRequest authRequest)
{
// Arrange
SetBaseServiceUri(sutProvider);
requestModel.RequestApproved = false; // Not an approval, so validation should be skipped
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
sutProvider.GetDependency<IAuthRequestService>()
.UpdateAuthRequestAsync(authRequest.Id, user.Id, requestModel)
.Returns(authRequest);
// Act
var result = await sutProvider.Sut
.Put(authRequest.Id, requestModel);
// Assert
Assert.NotNull(result);
Assert.IsType<AuthRequestResponseModel>(result);
}
[Theory, BitAutoData]
public async Task Put_WithApprovedRequest_ValidatesAndReturnsAuthRequest(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequestUpdateRequestModel requestModel,
AuthRequest currentAuthRequest,
AuthRequest updatedAuthRequest,
List<PendingAuthRequestDetails> pendingRequests)
{
// Arrange
SetBaseServiceUri(sutProvider);
requestModel.RequestApproved = true; // Approval triggers validation
currentAuthRequest.RequestDeviceIdentifier = "device-identifier-123";
// Setup pending requests - make the current request the most recent for its device
var mostRecentForDevice = new PendingAuthRequestDetails(currentAuthRequest, Guid.NewGuid());
pendingRequests.Add(mostRecentForDevice);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
// Setup validation dependencies
sutProvider.GetDependency<IAuthRequestService>()
.GetAuthRequestAsync(currentAuthRequest.Id, user.Id)
.Returns(currentAuthRequest);
sutProvider.GetDependency<IAuthRequestRepository>()
.GetManyPendingAuthRequestByUserId(user.Id)
.Returns(pendingRequests);
sutProvider.GetDependency<IAuthRequestService>()
.UpdateAuthRequestAsync(currentAuthRequest.Id, user.Id, requestModel)
.Returns(updatedAuthRequest);
// Act
var result = await sutProvider.Sut
.Put(currentAuthRequest.Id, requestModel);
// Assert
Assert.NotNull(result);
Assert.IsType<AuthRequestResponseModel>(result);
}
[Theory, BitAutoData]
public async Task Put_WithApprovedRequest_CurrentAuthRequestNotFound_ThrowsNotFoundException(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequestUpdateRequestModel requestModel,
Guid authRequestId)
{
// Arrange
requestModel.RequestApproved = true; // Approval triggers validation
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
// Current auth request not found
sutProvider.GetDependency<IAuthRequestService>()
.GetAuthRequestAsync(authRequestId, user.Id)
.Returns((AuthRequest)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.Put(authRequestId, requestModel));
}
[Theory, BitAutoData]
public async Task Put_WithApprovedRequest_NotMostRecentForDevice_ThrowsBadRequestException(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequestUpdateRequestModel requestModel,
AuthRequest currentAuthRequest,
List<PendingAuthRequestDetails> pendingRequests)
{
// Arrange
requestModel.RequestApproved = true; // Approval triggers validation
currentAuthRequest.RequestDeviceIdentifier = "device-identifier-123";
// Setup pending requests - make a different request the most recent for the same device
var differentAuthRequest = new AuthRequest
{
Id = Guid.NewGuid(), // Different ID than current request
RequestDeviceIdentifier = currentAuthRequest.RequestDeviceIdentifier,
UserId = user.Id,
Type = AuthRequestType.AuthenticateAndUnlock,
CreationDate = DateTime.UtcNow
};
var mostRecentForDevice = new PendingAuthRequestDetails(differentAuthRequest, Guid.NewGuid());
pendingRequests.Add(mostRecentForDevice);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
sutProvider.GetDependency<IAuthRequestService>()
.GetAuthRequestAsync(currentAuthRequest.Id, user.Id)
.Returns(currentAuthRequest);
sutProvider.GetDependency<IAuthRequestRepository>()
.GetManyPendingAuthRequestByUserId(user.Id)
.Returns(pendingRequests);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.Put(currentAuthRequest.Id, requestModel));
Assert.Equal("This request is no longer valid. Make sure to approve the most recent request.", exception.Message);
}
private void SetBaseServiceUri(SutProvider<AuthRequestsController> sutProvider)
{
sutProvider.GetDependency<IGlobalSettings>()
.BaseServiceUri
.Vault
.Returns(_testGlobalSettingsBaseUri);
}
}

View File

@@ -73,7 +73,7 @@ public class DevicesControllerTest
_deviceRepositoryMock.GetManyByUserIdWithDeviceAuth(userId).Returns(devicesWithPendingAuthData);
// Act
var result = await _sut.Get();
var result = await _sut.GetAll();
// Assert
Assert.NotNull(result);
@@ -94,6 +94,6 @@ public class DevicesControllerTest
_userServiceMock.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>()).Returns((Guid?)null);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.Get());
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.GetAll());
}
}

View File

@@ -0,0 +1,313 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Auth.Models.Request.Organizations;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Services;
using Bit.Core.Sso;
using Microsoft.Extensions.Localization;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Auth.Models.Request;
public class OrganizationSsoRequestModelTests
{
[Fact]
public void ToSsoConfig_WithOrganizationId_CreatesNewSsoConfig()
{
// Arrange
var organizationId = Guid.NewGuid();
var model = new OrganizationSsoRequestModel
{
Enabled = true,
Identifier = "test-identifier",
Data = new SsoConfigurationDataRequest
{
ConfigType = SsoType.OpenIdConnect,
Authority = "https://example.com",
ClientId = "test-client",
ClientSecret = "test-secret"
}
};
// Act
var result = model.ToSsoConfig(organizationId);
// Assert
Assert.NotNull(result);
Assert.Equal(organizationId, result.OrganizationId);
Assert.True(result.Enabled);
}
[Fact]
public void ToSsoConfig_WithExistingConfig_UpdatesExistingConfig()
{
// Arrange
var organizationId = Guid.NewGuid();
var existingConfig = new SsoConfig
{
Id = 1,
OrganizationId = organizationId,
Enabled = false
};
var model = new OrganizationSsoRequestModel
{
Enabled = true,
Identifier = "updated-identifier",
Data = new SsoConfigurationDataRequest
{
ConfigType = SsoType.Saml2,
IdpEntityId = "test-entity",
IdpSingleSignOnServiceUrl = "https://sso.example.com"
}
};
// Act
var result = model.ToSsoConfig(existingConfig);
// Assert
Assert.Same(existingConfig, result);
Assert.Equal(organizationId, result.OrganizationId);
Assert.True(result.Enabled);
}
}
public class SsoConfigurationDataRequestTests
{
private readonly TestI18nService _i18nService;
private readonly ValidationContext _validationContext;
public SsoConfigurationDataRequestTests()
{
_i18nService = new TestI18nService();
var serviceProvider = Substitute.For<IServiceProvider>();
serviceProvider.GetService(typeof(II18nService)).Returns(_i18nService);
_validationContext = new ValidationContext(new object(), serviceProvider, null);
}
[Fact]
public void ToConfigurationData_MapsProperties()
{
// Arrange
var model = new SsoConfigurationDataRequest
{
ConfigType = SsoType.OpenIdConnect,
MemberDecryptionType = MemberDecryptionType.KeyConnector,
Authority = "https://authority.example.com",
ClientId = "test-client-id",
ClientSecret = "test-client-secret",
IdpX509PublicCert = "-----BEGIN CERTIFICATE-----\nMIIC...test\n-----END CERTIFICATE-----",
SpOutboundSigningAlgorithm = null // Test default
};
// Act
var result = model.ToConfigurationData();
// Assert
Assert.Equal(SsoType.OpenIdConnect, result.ConfigType);
Assert.Equal(MemberDecryptionType.KeyConnector, result.MemberDecryptionType);
Assert.Equal("https://authority.example.com", result.Authority);
Assert.Equal("test-client-id", result.ClientId);
Assert.Equal("test-client-secret", result.ClientSecret);
Assert.Equal("MIIC...test", result.IdpX509PublicCert); // PEM headers stripped
Assert.Equal(SamlSigningAlgorithms.Sha256, result.SpOutboundSigningAlgorithm); // Default applied
Assert.Null(result.IdpArtifactResolutionServiceUrl); // Always null
}
[Fact]
public void KeyConnectorEnabled_Setter_UpdatesMemberDecryptionType()
{
// Arrange
var model = new SsoConfigurationDataRequest();
// Act & Assert
#pragma warning disable CS0618 // Type or member is obsolete
model.KeyConnectorEnabled = true;
Assert.Equal(MemberDecryptionType.KeyConnector, model.MemberDecryptionType);
model.KeyConnectorEnabled = false;
Assert.Equal(MemberDecryptionType.MasterPassword, model.MemberDecryptionType);
#pragma warning restore CS0618 // Type or member is obsolete
}
// Validation Tests
[Fact]
public void Validate_OpenIdConnect_ValidData_NoErrors()
{
// Arrange
var model = new SsoConfigurationDataRequest
{
ConfigType = SsoType.OpenIdConnect,
Authority = "https://example.com",
ClientId = "test-client",
ClientSecret = "test-secret"
};
// Act
var results = model.Validate(_validationContext).ToList();
// Assert
Assert.Empty(results);
}
[Theory]
[InlineData("", "test-client", "test-secret", "AuthorityValidationError")]
[InlineData("https://example.com", "", "test-secret", "ClientIdValidationError")]
[InlineData("https://example.com", "test-client", "", "ClientSecretValidationError")]
public void Validate_OpenIdConnect_MissingRequiredFields_ReturnsErrors(string authority, string clientId, string clientSecret, string expectedError)
{
// Arrange
var model = new SsoConfigurationDataRequest
{
ConfigType = SsoType.OpenIdConnect,
Authority = authority,
ClientId = clientId,
ClientSecret = clientSecret
};
// Act
var results = model.Validate(_validationContext).ToList();
// Assert
Assert.Single(results);
Assert.Equal(expectedError, results[0].ErrorMessage);
}
[Fact]
public void Validate_Saml2_ValidData_NoErrors()
{
// Arrange
var model = new SsoConfigurationDataRequest
{
ConfigType = SsoType.Saml2,
IdpEntityId = "https://idp.example.com",
IdpSingleSignOnServiceUrl = "https://sso.example.com",
IdpSingleLogoutServiceUrl = "https://logout.example.com"
};
// Act
var results = model.Validate(_validationContext).ToList();
// Assert
Assert.Empty(results);
}
[Theory]
[InlineData("", "https://sso.example.com", "IdpEntityIdValidationError")]
[InlineData("not-a-valid-uri", "", "IdpSingleSignOnServiceUrlValidationError")]
public void Validate_Saml2_MissingRequiredFields_ReturnsErrors(string entityId, string signOnUrl, string expectedError)
{
// Arrange
var model = new SsoConfigurationDataRequest
{
ConfigType = SsoType.Saml2,
IdpEntityId = entityId,
IdpSingleSignOnServiceUrl = signOnUrl
};
// Act
var results = model.Validate(_validationContext).ToList();
// Assert
Assert.Contains(results, r => r.ErrorMessage == expectedError);
}
[Theory]
[InlineData("not-a-url")]
[InlineData("ftp://example.com")]
[InlineData("https://example.com<script>")]
[InlineData("https://example.com\"onclick")]
public void Validate_Saml2_InvalidUrls_ReturnsErrors(string invalidUrl)
{
// Arrange
var model = new SsoConfigurationDataRequest
{
ConfigType = SsoType.Saml2,
IdpEntityId = "https://idp.example.com",
IdpSingleSignOnServiceUrl = invalidUrl,
IdpSingleLogoutServiceUrl = invalidUrl
};
// Act
var results = model.Validate(_validationContext).ToList();
// Assert
Assert.Contains(results, r => r.ErrorMessage == "IdpSingleSignOnServiceUrlInvalid");
Assert.Contains(results, r => r.ErrorMessage == "IdpSingleLogoutServiceUrlInvalid");
}
[Fact]
public void Validate_Saml2_MissingSignOnUrl_AlwaysReturnsError()
{
// Arrange - SignOnUrl is always required for SAML2, regardless of EntityId format
var model = new SsoConfigurationDataRequest
{
ConfigType = SsoType.Saml2,
IdpEntityId = "https://idp.example.com", // Valid URI
IdpSingleSignOnServiceUrl = "" // Missing - always causes error
};
// Act
var results = model.Validate(_validationContext).ToList();
// Assert - Should always fail validation when SignOnUrl is missing
Assert.Contains(results, r => r.ErrorMessage == "IdpSingleSignOnServiceUrlValidationError");
}
[Fact]
public void Validate_Saml2_InvalidCertificate_ReturnsError()
{
// Arrange
var model = new SsoConfigurationDataRequest
{
ConfigType = SsoType.Saml2,
IdpEntityId = "https://idp.example.com",
IdpSingleSignOnServiceUrl = "https://sso.example.com",
IdpX509PublicCert = "invalid-certificate-data"
};
// Act
var results = model.Validate(_validationContext).ToList();
// Assert
Assert.Contains(results, r => r.ErrorMessage.Contains("IdpX509PublicCert") && r.ErrorMessage.Contains("ValidationError"));
}
// TODO: On server, make public certificate required for SAML2 SSO: https://bitwarden.atlassian.net/browse/PM-26028
[Fact]
public void Validate_Saml2_EmptyCertificate_PassesValidation()
{
// Arrange
var model = new SsoConfigurationDataRequest
{
ConfigType = SsoType.Saml2,
IdpEntityId = "https://idp.example.com",
IdpSingleSignOnServiceUrl = "https://sso.example.com",
IdpX509PublicCert = ""
};
// Act
var results = model.Validate(_validationContext).ToList();
// Assert
Assert.DoesNotContain(results, r => r.MemberNames.Contains("IdpX509PublicCert"));
}
private class TestI18nService : I18nService
{
public TestI18nService() : base(CreateMockLocalizerFactory()) { }
private static IStringLocalizerFactory CreateMockLocalizerFactory()
{
var factory = Substitute.For<IStringLocalizerFactory>();
var localizer = Substitute.For<IStringLocalizer>();
localizer[Arg.Any<string>()].Returns(callInfo => new LocalizedString(callInfo.Arg<string>(), callInfo.Arg<string>()));
localizer[Arg.Any<string>(), Arg.Any<object[]>()].Returns(callInfo => new LocalizedString(callInfo.Arg<string>(), callInfo.Arg<string>()));
factory.Create(Arg.Any<string>(), Arg.Any<string>()).Returns(localizer);
return factory;
}
}
}

View File

@@ -0,0 +1,132 @@
using Bit.Api.Billing.Attributes;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Api;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Billing.Attributes;
public class InjectOrganizationAttributeTests
{
private readonly IOrganizationRepository _organizationRepository;
private readonly ActionExecutionDelegate _next;
private readonly ActionExecutingContext _context;
private readonly Organization _organization;
private readonly Guid _organizationId;
public InjectOrganizationAttributeTests()
{
_organizationRepository = Substitute.For<IOrganizationRepository>();
_organizationId = Guid.NewGuid();
_organization = new Organization { Id = _organizationId };
var httpContext = new DefaultHttpContext();
var services = new ServiceCollection();
services.AddScoped(_ => _organizationRepository);
httpContext.RequestServices = services.BuildServiceProvider();
var routeData = new RouteData { Values = { ["organizationId"] = _organizationId.ToString() } };
var actionContext = new ActionContext(
httpContext,
routeData,
new ActionDescriptor(),
new ModelStateDictionary()
);
_next = () => Task.FromResult(new ActionExecutedContext(
actionContext,
new List<IFilterMetadata>(),
new object()));
_context = new ActionExecutingContext(
actionContext,
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
new object());
}
[Fact]
public async Task OnActionExecutionAsync_WithExistingOrganization_InjectsOrganization()
{
var attribute = new InjectOrganizationAttribute();
_organizationRepository.GetByIdAsync(_organizationId)
.Returns(_organization);
var parameter = new ParameterDescriptor
{
Name = "organization",
ParameterType = typeof(Organization)
};
_context.ActionDescriptor.Parameters = [parameter];
await attribute.OnActionExecutionAsync(_context, _next);
Assert.Equal(_organization, _context.ActionArguments["organization"]);
}
[Fact]
public async Task OnActionExecutionAsync_WithNonExistentOrganization_ReturnsNotFound()
{
var attribute = new InjectOrganizationAttribute();
_organizationRepository.GetByIdAsync(_organizationId)
.Returns((Organization)null);
await attribute.OnActionExecutionAsync(_context, _next);
Assert.IsType<NotFoundObjectResult>(_context.Result);
var result = (NotFoundObjectResult)_context.Result;
Assert.IsType<ErrorResponseModel>(result.Value);
Assert.Equal("Organization not found.", ((ErrorResponseModel)result.Value).Message);
}
[Fact]
public async Task OnActionExecutionAsync_WithInvalidOrganizationId_ReturnsBadRequest()
{
var attribute = new InjectOrganizationAttribute();
_context.RouteData.Values["organizationId"] = "not-a-guid";
await attribute.OnActionExecutionAsync(_context, _next);
Assert.IsType<BadRequestObjectResult>(_context.Result);
var result = (BadRequestObjectResult)_context.Result;
Assert.IsType<ErrorResponseModel>(result.Value);
Assert.Equal("Route parameter 'organizationId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message);
}
[Fact]
public async Task OnActionExecutionAsync_WithMissingOrganizationId_ReturnsBadRequest()
{
var attribute = new InjectOrganizationAttribute();
_context.RouteData.Values.Clear();
await attribute.OnActionExecutionAsync(_context, _next);
Assert.IsType<BadRequestObjectResult>(_context.Result);
var result = (BadRequestObjectResult)_context.Result;
Assert.IsType<ErrorResponseModel>(result.Value);
Assert.Equal("Route parameter 'organizationId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message);
}
[Fact]
public async Task OnActionExecutionAsync_WithoutOrganizationParameter_ContinuesExecution()
{
var attribute = new InjectOrganizationAttribute();
_organizationRepository.GetByIdAsync(_organizationId)
.Returns(_organization);
_context.ActionDescriptor.Parameters = Array.Empty<ParameterDescriptor>();
await attribute.OnActionExecutionAsync(_context, _next);
Assert.Empty(_context.ActionArguments);
}
}

View File

@@ -0,0 +1,190 @@
using Bit.Api.Billing.Attributes;
using Bit.Api.Models.Public.Response;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Billing.Attributes;
public class InjectProviderAttributeTests
{
private readonly IProviderRepository _providerRepository;
private readonly ICurrentContext _currentContext;
private readonly ActionExecutionDelegate _next;
private readonly ActionExecutingContext _context;
private readonly Provider _provider;
private readonly Guid _providerId;
public InjectProviderAttributeTests()
{
_providerRepository = Substitute.For<IProviderRepository>();
_currentContext = Substitute.For<ICurrentContext>();
_providerId = Guid.NewGuid();
_provider = new Provider { Id = _providerId };
var httpContext = new DefaultHttpContext();
var services = new ServiceCollection();
services.AddScoped(_ => _providerRepository);
services.AddScoped(_ => _currentContext);
httpContext.RequestServices = services.BuildServiceProvider();
var routeData = new RouteData { Values = { ["providerId"] = _providerId.ToString() } };
var actionContext = new ActionContext(
httpContext,
routeData,
new ActionDescriptor(),
new ModelStateDictionary()
);
_next = () => Task.FromResult(new ActionExecutedContext(
actionContext,
new List<IFilterMetadata>(),
new object()));
_context = new ActionExecutingContext(
actionContext,
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
new object());
}
[Fact]
public async Task OnActionExecutionAsync_WithExistingProvider_InjectsProvider()
{
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
_currentContext.ProviderProviderAdmin(_providerId).Returns(true);
var parameter = new ParameterDescriptor
{
Name = "provider",
ParameterType = typeof(Provider)
};
_context.ActionDescriptor.Parameters = [parameter];
await attribute.OnActionExecutionAsync(_context, _next);
Assert.Equal(_provider, _context.ActionArguments["provider"]);
}
[Fact]
public async Task OnActionExecutionAsync_WithNonExistentProvider_ReturnsNotFound()
{
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
_providerRepository.GetByIdAsync(_providerId).Returns((Provider)null);
await attribute.OnActionExecutionAsync(_context, _next);
Assert.IsType<NotFoundObjectResult>(_context.Result);
var result = (NotFoundObjectResult)_context.Result;
Assert.IsType<ErrorResponseModel>(result.Value);
Assert.Equal("Provider not found.", ((ErrorResponseModel)result.Value).Message);
}
[Fact]
public async Task OnActionExecutionAsync_WithInvalidProviderId_ReturnsBadRequest()
{
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
_context.RouteData.Values["providerId"] = "not-a-guid";
await attribute.OnActionExecutionAsync(_context, _next);
Assert.IsType<BadRequestObjectResult>(_context.Result);
var result = (BadRequestObjectResult)_context.Result;
Assert.IsType<ErrorResponseModel>(result.Value);
Assert.Equal("Route parameter 'providerId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message);
}
[Fact]
public async Task OnActionExecutionAsync_WithMissingProviderId_ReturnsBadRequest()
{
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
_context.RouteData.Values.Clear();
await attribute.OnActionExecutionAsync(_context, _next);
Assert.IsType<BadRequestObjectResult>(_context.Result);
var result = (BadRequestObjectResult)_context.Result;
Assert.IsType<ErrorResponseModel>(result.Value);
Assert.Equal("Route parameter 'providerId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message);
}
[Fact]
public async Task OnActionExecutionAsync_WithoutProviderParameter_ContinuesExecution()
{
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
_currentContext.ProviderProviderAdmin(_providerId).Returns(true);
_context.ActionDescriptor.Parameters = Array.Empty<ParameterDescriptor>();
await attribute.OnActionExecutionAsync(_context, _next);
Assert.Empty(_context.ActionArguments);
}
[Fact]
public async Task OnActionExecutionAsync_UnauthorizedProviderAdmin_ReturnsUnauthorized()
{
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
_currentContext.ProviderProviderAdmin(_providerId).Returns(false);
await attribute.OnActionExecutionAsync(_context, _next);
Assert.IsType<UnauthorizedObjectResult>(_context.Result);
var result = (UnauthorizedObjectResult)_context.Result;
Assert.IsType<ErrorResponseModel>(result.Value);
Assert.Equal("Unauthorized.", ((ErrorResponseModel)result.Value).Message);
}
[Fact]
public async Task OnActionExecutionAsync_UnauthorizedServiceUser_ReturnsUnauthorized()
{
var attribute = new InjectProviderAttribute(ProviderUserType.ServiceUser);
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
_currentContext.ProviderUser(_providerId).Returns(false);
await attribute.OnActionExecutionAsync(_context, _next);
Assert.IsType<UnauthorizedObjectResult>(_context.Result);
var result = (UnauthorizedObjectResult)_context.Result;
Assert.IsType<ErrorResponseModel>(result.Value);
Assert.Equal("Unauthorized.", ((ErrorResponseModel)result.Value).Message);
}
[Fact]
public async Task OnActionExecutionAsync_AuthorizedProviderAdmin_Succeeds()
{
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
_currentContext.ProviderProviderAdmin(_providerId).Returns(true);
await attribute.OnActionExecutionAsync(_context, _next);
Assert.Null(_context.Result);
}
[Fact]
public async Task OnActionExecutionAsync_AuthorizedServiceUser_Succeeds()
{
var attribute = new InjectProviderAttribute(ProviderUserType.ServiceUser);
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
_currentContext.ProviderUser(_providerId).Returns(true);
await attribute.OnActionExecutionAsync(_context, _next);
Assert.Null(_context.Result);
}
}

View File

@@ -0,0 +1,129 @@
using System.Security.Claims;
using Bit.Api.Billing.Attributes;
using Bit.Core.Entities;
using Bit.Core.Models.Api;
using Bit.Core.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Billing.Attributes;
public class InjectUserAttributesTests
{
private readonly IUserService _userService;
private readonly ActionExecutionDelegate _next;
private readonly ActionExecutingContext _context;
private readonly User _user;
public InjectUserAttributesTests()
{
_userService = Substitute.For<IUserService>();
_user = new User { Id = Guid.NewGuid() };
var httpContext = new DefaultHttpContext();
var services = new ServiceCollection();
services.AddScoped(_ => _userService);
httpContext.RequestServices = services.BuildServiceProvider();
var actionContext = new ActionContext(
httpContext,
new RouteData(),
new ActionDescriptor(),
new ModelStateDictionary()
);
_next = () => Task.FromResult(new ActionExecutedContext(
actionContext,
new List<IFilterMetadata>(),
new object()));
_context = new ActionExecutingContext(
actionContext,
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
new object());
}
[Fact]
public async Task OnActionExecutionAsync_WithAuthorizedUser_InjectsUser()
{
var attribute = new InjectUserAttribute();
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(_user);
var parameter = new ParameterDescriptor
{
Name = "user",
ParameterType = typeof(User)
};
_context.ActionDescriptor.Parameters = [parameter];
await attribute.OnActionExecutionAsync(_context, _next);
Assert.Equal(_user, _context.ActionArguments["user"]);
}
[Fact]
public async Task OnActionExecutionAsync_WithUnauthorizedUser_ReturnsUnauthorized()
{
var attribute = new InjectUserAttribute();
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns((User)null);
await attribute.OnActionExecutionAsync(_context, _next);
Assert.IsType<UnauthorizedObjectResult>(_context.Result);
var result = (UnauthorizedObjectResult)_context.Result;
Assert.IsType<ErrorResponseModel>(result.Value);
Assert.Equal("Unauthorized.", ((ErrorResponseModel)result.Value).Message);
}
[Fact]
public async Task OnActionExecutionAsync_WithoutUserParameter_ContinuesExecution()
{
var attribute = new InjectUserAttribute();
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(_user);
_context.ActionDescriptor.Parameters = Array.Empty<ParameterDescriptor>();
await attribute.OnActionExecutionAsync(_context, _next);
Assert.Empty(_context.ActionArguments);
}
[Fact]
public async Task OnActionExecutionAsync_WithMultipleParameters_InjectsUserCorrectly()
{
var attribute = new InjectUserAttribute();
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(_user);
var parameters = new[]
{
new ParameterDescriptor
{
Name = "otherParam",
ParameterType = typeof(string)
},
new ParameterDescriptor
{
Name = "user",
ParameterType = typeof(User)
}
};
_context.ActionDescriptor.Parameters = parameters;
await attribute.OnActionExecutionAsync(_context, _next);
Assert.Single(_context.ActionArguments);
Assert.Equal(_user, _context.ActionArguments["user"]);
}
}

View File

@@ -2,7 +2,8 @@
using Bit.Api.Billing.Models.Responses;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Context;
using Bit.Core.Repositories;
using Bit.Core.Services;

View File

@@ -10,8 +10,9 @@ 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.Organizations.Queries;
using Bit.Core.Billing.Organizations.Repositories;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
@@ -19,7 +20,6 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -40,7 +40,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IPaymentService _paymentService;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IUserService _userService;
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery;
private readonly ILicensingService _licensingService;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;
@@ -64,7 +64,7 @@ public class OrganizationsControllerTests : IDisposable
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
Substitute.For<ISsoConfigService>();
_userService = Substitute.For<IUserService>();
_cloudGetOrganizationLicenseQuery = Substitute.For<ICloudGetOrganizationLicenseQuery>();
_getCloudOrganizationLicenseQuery = Substitute.For<IGetCloudOrganizationLicenseQuery>();
_licensingService = Substitute.For<ILicensingService>();
_updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();
_upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();
@@ -81,7 +81,7 @@ public class OrganizationsControllerTests : IDisposable
_userService,
_paymentService,
_currentContext,
_cloudGetOrganizationLicenseQuery,
_getCloudOrganizationLicenseQuery,
_globalSettings,
_licensingService,
_updateSecretsManagerSubscriptionCommand,

View File

@@ -1,8 +1,6 @@
using Bit.Api.Billing.Controllers;
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses;
using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
@@ -346,9 +344,6 @@ public class ProviderBillingControllerTests
}
};
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe)
.Returns(true);
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
foreach (var providerPlan in providerPlans)

View File

@@ -1,315 +0,0 @@
using Bit.Api.Billing.Queries.Organizations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Services;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Stripe.TestHelpers;
using Xunit;
namespace Bit.Api.Test.Billing.Queries.Organizations;
[SutProviderCustomize]
public class OrganizationWarningsQueryTests
{
private static readonly string[] _requiredExpansions = ["customer", "latest_invoice", "test_clock"];
[Theory, BitAutoData]
public async Task Run_NoSubscription_NoWarnings(
Organization organization,
SutProvider<OrganizationWarningsQuery> sutProvider)
{
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.ReturnsNull();
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
FreeTrial: null,
InactiveSubscription: null,
ResellerRenewal: null
});
}
[Theory, BitAutoData]
public async Task Run_Has_FreeTrialWarning(
Organization organization,
SutProvider<OrganizationWarningsQuery> sutProvider)
{
var now = DateTime.UtcNow;
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Trialing,
TrialEnd = now.AddDays(7),
Customer = new Customer
{
InvoiceSettings = new CustomerInvoiceSettings(),
Metadata = new Dictionary<string, string>()
},
TestClock = new TestClock
{
FrozenTime = now
}
});
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
FreeTrial.RemainingTrialDays: 7
});
}
[Theory, BitAutoData]
public async Task Run_Has_InactiveSubscriptionWarning_ContactProvider(
Organization organization,
SutProvider<OrganizationWarningsQuery> sutProvider)
{
organization.Enabled = false;
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Unpaid
});
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)
.Returns(new Provider());
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
InactiveSubscription.Resolution: "contact_provider"
});
}
[Theory, BitAutoData]
public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethod(
Organization organization,
SutProvider<OrganizationWarningsQuery> sutProvider)
{
organization.Enabled = false;
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Unpaid
});
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
InactiveSubscription.Resolution: "add_payment_method"
});
}
[Theory, BitAutoData]
public async Task Run_Has_InactiveSubscriptionWarning_Resubscribe(
Organization organization,
SutProvider<OrganizationWarningsQuery> sutProvider)
{
organization.Enabled = false;
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Canceled
});
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
InactiveSubscription.Resolution: "resubscribe"
});
}
[Theory, BitAutoData]
public async Task Run_Has_InactiveSubscriptionWarning_ContactOwner(
Organization organization,
SutProvider<OrganizationWarningsQuery> sutProvider)
{
organization.Enabled = false;
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
Status = StripeConstants.SubscriptionStatus.Unpaid
});
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(false);
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
InactiveSubscription.Resolution: "contact_owner"
});
}
[Theory, BitAutoData]
public async Task Run_Has_ResellerRenewalWarning_Upcoming(
Organization organization,
SutProvider<OrganizationWarningsQuery> sutProvider)
{
var now = DateTime.UtcNow;
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
Status = StripeConstants.SubscriptionStatus.Active,
CurrentPeriodEnd = now.AddDays(10),
TestClock = new TestClock
{
FrozenTime = now
}
});
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)
.Returns(new Provider
{
Type = ProviderType.Reseller
});
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
ResellerRenewal.Type: "upcoming"
});
Assert.Equal(now.AddDays(10), response.ResellerRenewal.Upcoming!.RenewalDate);
}
[Theory, BitAutoData]
public async Task Run_Has_ResellerRenewalWarning_Issued(
Organization organization,
SutProvider<OrganizationWarningsQuery> sutProvider)
{
var now = DateTime.UtcNow;
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
Status = StripeConstants.SubscriptionStatus.Active,
LatestInvoice = new Invoice
{
Status = StripeConstants.InvoiceStatus.Open,
DueDate = now.AddDays(30),
Created = now
},
TestClock = new TestClock
{
FrozenTime = now
}
});
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)
.Returns(new Provider
{
Type = ProviderType.Reseller
});
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
ResellerRenewal.Type: "issued"
});
Assert.Equal(now, response.ResellerRenewal.Issued!.IssuedDate);
Assert.Equal(now.AddDays(30), response.ResellerRenewal.Issued!.DueDate);
}
[Theory, BitAutoData]
public async Task Run_Has_ResellerRenewalWarning_PastDue(
Organization organization,
SutProvider<OrganizationWarningsQuery> sutProvider)
{
var now = DateTime.UtcNow;
const string subscriptionId = "subscription_id";
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
Id = subscriptionId,
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
Status = StripeConstants.SubscriptionStatus.PastDue,
TestClock = new TestClock
{
FrozenTime = now
}
});
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)
.Returns(new Provider
{
Type = ProviderType.Reseller
});
var dueDate = now.AddDays(-10);
sutProvider.GetDependency<IStripeAdapter>().InvoiceSearchAsync(Arg.Is<InvoiceSearchOptions>(options =>
options.Query == $"subscription:'{subscriptionId}' status:'open'")).Returns([
new Invoice { DueDate = dueDate, Created = dueDate.AddDays(-30) }
]);
var response = await sutProvider.Sut.Run(organization);
Assert.True(response is
{
ResellerRenewal.Type: "past_due"
});
Assert.Equal(dueDate.AddDays(30), response.ResellerRenewal.PastDue!.SuspensionDate);
}
}

View File

@@ -22,7 +22,7 @@ namespace Bit.Api.Test.Controllers;
public class CollectionsControllerTests
{
[Theory, BitAutoData]
public async Task Post_Success(Organization organization, CollectionRequestModel collectionRequest,
public async Task Post_Success(Organization organization, CreateCollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider)
{
Collection ExpectedCollection() => Arg.Is<Collection>(c =>
@@ -46,9 +46,10 @@ public class CollectionsControllerTests
}
[Theory, BitAutoData]
public async Task Put_Success(Collection collection, CollectionRequestModel collectionRequest,
public async Task Put_Success(Collection collection, UpdateCollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider)
{
collection.DefaultUserCollectionEmail = null;
Collection ExpectedCollection() => Arg.Is<Collection>(c => c.Id == collection.Id &&
c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&
c.OrganizationId == collection.OrganizationId);
@@ -72,7 +73,7 @@ public class CollectionsControllerTests
}
[Theory, BitAutoData]
public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, CollectionRequestModel collectionRequest,
public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, UpdateCollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider)
{
sutProvider.GetDependency<IAuthorizationService>()
@@ -173,12 +174,12 @@ public class CollectionsControllerTests
.Returns(AuthorizationResult.Success());
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByOrganizationIdAsync(organization.Id)
.GetManySharedCollectionsByOrganizationIdAsync(organization.Id)
.Returns(collections);
var response = await sutProvider.Sut.Get(organization.Id);
var response = await sutProvider.Sut.GetAll(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdAsync(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManySharedCollectionsByOrganizationIdAsync(organization.Id);
Assert.Equal(collections.Count, response.Data.Count());
}
@@ -218,7 +219,7 @@ public class CollectionsControllerTests
.GetManyByUserIdAsync(userId)
.Returns(collections);
var result = await sutProvider.Sut.Get(organization.Id);
var result = await sutProvider.Sut.GetAll(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdAsync(organization.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(userId);
@@ -484,4 +485,176 @@ public class CollectionsControllerTests
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
.AddAccessAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task Put_With_NonNullName_DoesNotPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider)
{
// Arrange
var newName = "new name";
var originalName = "original name";
existingCollection.Name = originalName;
existingCollection.DefaultUserCollectionEmail = null;
collectionRequest.Name = newName;
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(existingCollection.Id)
.Returns(existingCollection);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
existingCollection,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))
.Returns(AuthorizationResult.Success());
// Act
await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);
// Assert
await sutProvider.GetDependency<IUpdateCollectionCommand>()
.Received(1)
.UpdateAsync(
Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == newName),
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Any<IEnumerable<CollectionAccessSelection>>());
}
[Theory, BitAutoData]
public async Task Put_WithNullName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider)
{
// Arrange
var originalName = "original name";
existingCollection.Name = originalName;
existingCollection.DefaultUserCollectionEmail = null;
collectionRequest.Name = null;
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(existingCollection.Id)
.Returns(existingCollection);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
existingCollection,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))
.Returns(AuthorizationResult.Success());
// Act
await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);
// Assert
await sutProvider.GetDependency<IUpdateCollectionCommand>()
.Received(1)
.UpdateAsync(
Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName),
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Any<IEnumerable<CollectionAccessSelection>>());
}
[Theory, BitAutoData]
public async Task Put_WithDefaultUserCollectionEmail_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider)
{
// Arrange
var originalName = "original name";
var defaultUserCollectionEmail = "user@email.com";
existingCollection.Name = originalName;
existingCollection.DefaultUserCollectionEmail = defaultUserCollectionEmail;
collectionRequest.Name = "new name";
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(existingCollection.Id)
.Returns(existingCollection);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
existingCollection,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))
.Returns(AuthorizationResult.Success());
// Act
await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);
// Assert
await sutProvider.GetDependency<IUpdateCollectionCommand>()
.Received(1)
.UpdateAsync(
Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName && c.DefaultUserCollectionEmail == defaultUserCollectionEmail),
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Any<IEnumerable<CollectionAccessSelection>>());
}
[Theory, BitAutoData]
public async Task Put_WithEmptyName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider)
{
// Arrange
var originalName = "original name";
existingCollection.Name = originalName;
existingCollection.DefaultUserCollectionEmail = null;
collectionRequest.Name = ""; // Empty string
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(existingCollection.Id)
.Returns(existingCollection);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
existingCollection,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))
.Returns(AuthorizationResult.Success());
// Act
await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);
// Assert
await sutProvider.GetDependency<IUpdateCollectionCommand>()
.Received(1)
.UpdateAsync(
Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName),
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Any<IEnumerable<CollectionAccessSelection>>());
}
[Theory, BitAutoData]
public async Task Put_WithWhitespaceOnlyName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,
SutProvider<CollectionsController> sutProvider)
{
// Arrange
var originalName = "original name";
existingCollection.Name = originalName;
existingCollection.DefaultUserCollectionEmail = null;
collectionRequest.Name = " "; // Whitespace only
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(existingCollection.Id)
.Returns(existingCollection);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
existingCollection,
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))
.Returns(AuthorizationResult.Success());
// Act
await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);
// Assert
await sutProvider.GetDependency<IUpdateCollectionCommand>()
.Received(1)
.UpdateAsync(
Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName),
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Any<IEnumerable<CollectionAccessSelection>>());
}
}

View File

@@ -73,13 +73,13 @@ public class PoliciesControllerTests
// Assert that the data is deserialized correctly into a Dictionary<string, object>
// for all MasterPasswordPolicyData properties
Assert.Equal(mpPolicyData.MinComplexity, ((JsonElement)result.Data["MinComplexity"]).GetInt32());
Assert.Equal(mpPolicyData.MinLength, ((JsonElement)result.Data["MinLength"]).GetInt32());
Assert.Equal(mpPolicyData.RequireLower, ((JsonElement)result.Data["RequireLower"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireUpper, ((JsonElement)result.Data["RequireUpper"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireNumbers, ((JsonElement)result.Data["RequireNumbers"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireSpecial, ((JsonElement)result.Data["RequireSpecial"]).GetBoolean());
Assert.Equal(mpPolicyData.EnforceOnLogin, ((JsonElement)result.Data["EnforceOnLogin"]).GetBoolean());
Assert.Equal(mpPolicyData.MinComplexity, ((JsonElement)result.Data["minComplexity"]).GetInt32());
Assert.Equal(mpPolicyData.MinLength, ((JsonElement)result.Data["minLength"]).GetInt32());
Assert.Equal(mpPolicyData.RequireLower, ((JsonElement)result.Data["requireLower"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireUpper, ((JsonElement)result.Data["requireUpper"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireNumbers, ((JsonElement)result.Data["requireNumbers"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireSpecial, ((JsonElement)result.Data["requireSpecial"]).GetBoolean());
Assert.Equal(mpPolicyData.EnforceOnLogin, ((JsonElement)result.Data["enforceOnLogin"]).GetBoolean());
}

File diff suppressed because it is too large Load Diff

View File

@@ -142,142 +142,4 @@ public class ReportsControllerTests
_.OrganizationId == request.OrganizationId &&
_.PasswordHealthReportApplicationIds == request.PasswordHealthReportApplicationIds));
}
[Theory, BitAutoData]
public async Task AddOrganizationReportAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var request = new AddOrganizationReportRequest
{
OrganizationId = Guid.NewGuid(),
ReportData = "Report Data",
Date = DateTime.UtcNow
};
await sutProvider.Sut.AddOrganizationReport(request);
// Assert
_ = sutProvider.GetDependency<IAddOrganizationReportCommand>()
.Received(1)
.AddOrganizationReportAsync(Arg.Is<AddOrganizationReportRequest>(_ =>
_.OrganizationId == request.OrganizationId &&
_.ReportData == request.ReportData &&
_.Date == request.Date));
}
[Theory, BitAutoData]
public async Task AddOrganizationReportAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var request = new AddOrganizationReportRequest
{
OrganizationId = Guid.NewGuid(),
ReportData = "Report Data",
Date = DateTime.UtcNow
};
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.AddOrganizationReport(request));
// Assert
_ = sutProvider.GetDependency<IAddOrganizationReportCommand>()
.Received(0);
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var request = new DropOrganizationReportRequest
{
OrganizationId = Guid.NewGuid(),
OrganizationReportIds = new List<Guid> { Guid.NewGuid(), Guid.NewGuid() }
};
await sutProvider.Sut.DropOrganizationReport(request);
// Assert
_ = sutProvider.GetDependency<IDropOrganizationReportCommand>()
.Received(1)
.DropOrganizationReportAsync(Arg.Is<DropOrganizationReportRequest>(_ =>
_.OrganizationId == request.OrganizationId &&
_.OrganizationReportIds.SequenceEqual(request.OrganizationReportIds)));
}
[Theory, BitAutoData]
public async Task DropOrganizationReportAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var request = new DropOrganizationReportRequest
{
OrganizationId = Guid.NewGuid(),
OrganizationReportIds = new List<Guid> { Guid.NewGuid(), Guid.NewGuid() }
};
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.DropOrganizationReport(request));
// Assert
_ = sutProvider.GetDependency<IDropOrganizationReportCommand>()
.Received(0);
}
[Theory, BitAutoData]
public async Task GetOrganizationReportAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var orgId = Guid.NewGuid();
var result = await sutProvider.Sut.GetOrganizationReports(orgId);
// Assert
_ = sutProvider.GetDependency<IGetOrganizationReportQuery>()
.Received(1)
.GetOrganizationReportAsync(Arg.Is<Guid>(_ => _ == orgId));
}
[Theory, BitAutoData]
public async Task GetOrganizationReportAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var orgId = Guid.NewGuid();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetOrganizationReports(orgId));
// Assert
_ = sutProvider.GetDependency<IGetOrganizationReportQuery>()
.Received(0);
}
[Theory, BitAutoData]
public async Task GetLastestOrganizationReportAsync_withAccess_success(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
// Act
var orgId = Guid.NewGuid();
var result = await sutProvider.Sut.GetLatestOrganizationReport(orgId);
// Assert
_ = sutProvider.GetDependency<IGetOrganizationReportQuery>()
.Received(1)
.GetLatestOrganizationReportAsync(Arg.Is<Guid>(_ => _ == orgId));
}
[Theory, BitAutoData]
public async Task GetLastestOrganizationReportAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
// Act
var orgId = Guid.NewGuid();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetLatestOrganizationReport(orgId));
// Assert
_ = sutProvider.GetDependency<IGetOrganizationReportQuery>()
.Received(0);
}
}

View File

@@ -18,7 +18,7 @@ public class MasterPasswordUnlockDataModelTests
[InlineData(KdfType.Argon2id, 3, 64, 4)]
public void Validate_Success(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
var model = new MasterPasswordUnlockDataModel
var model = new MasterPasswordUnlockAndAuthenticationDataModel
{
KdfType = kdfType,
KdfIterations = kdfIterations,
@@ -43,7 +43,7 @@ public class MasterPasswordUnlockDataModelTests
[InlineData((KdfType)2, 2, 64, 4)]
public void Validate_Failure(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
var model = new MasterPasswordUnlockDataModel
var model = new MasterPasswordUnlockAndAuthenticationDataModel
{
KdfType = kdfType,
KdfIterations = kdfIterations,
@@ -59,7 +59,7 @@ public class MasterPasswordUnlockDataModelTests
Assert.NotNull(result.First().ErrorMessage);
}
private static List<ValidationResult> Validate(MasterPasswordUnlockDataModel model)
private static List<ValidationResult> Validate(MasterPasswordUnlockAndAuthenticationDataModel model)
{
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, new ValidationContext(model), results, true);

View File

@@ -33,8 +33,9 @@ public class WebAuthnLoginKeyRotationValidatorTests
{
Id = guid,
SupportsPrf = true,
EncryptedPublicKey = "TestKey",
EncryptedUserKey = "Test"
EncryptedPublicKey = "TestPublicKey",
EncryptedUserKey = "TestUserKey",
EncryptedPrivateKey = "TestPrivateKey"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)
.Returns(new List<WebAuthnCredential> { data });
@@ -45,8 +46,12 @@ public class WebAuthnLoginKeyRotationValidatorTests
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_DoesNotSupportPRF_Ignores(
[BitAutoData(false, null, null, null)]
[BitAutoData(true, null, "TestPublicKey", "TestPrivateKey")]
[BitAutoData(true, "TestUserKey", null, "TestPrivateKey")]
[BitAutoData(true, "TestUserKey", "TestPublicKey", null)]
public async Task ValidateAsync_NotEncryptedOrPrfNotSupported_Ignores(
bool supportsPrf, string encryptedUserKey, string encryptedPublicKey, string encryptedPrivateKey,
SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,
IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)
{
@@ -58,7 +63,14 @@ public class WebAuthnLoginKeyRotationValidatorTests
EncryptedPublicKey = e.EncryptedPublicKey,
}).ToList();
var data = new WebAuthnCredential { Id = guid, EncryptedUserKey = "Test", EncryptedPublicKey = "TestKey" };
var data = new WebAuthnCredential
{
Id = guid,
SupportsPrf = supportsPrf,
EncryptedUserKey = encryptedUserKey,
EncryptedPublicKey = encryptedPublicKey,
EncryptedPrivateKey = encryptedPrivateKey
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)
.Returns(new List<WebAuthnCredential> { data });
@@ -69,7 +81,7 @@ public class WebAuthnLoginKeyRotationValidatorTests
[Theory]
[BitAutoData]
public async Task ValidateAsync_WrongWebAuthnKeys_Throws(
public async Task ValidateAsync_WebAuthnKeysNotMatchingExisting_Throws(
SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,
IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)
{
@@ -84,10 +96,12 @@ public class WebAuthnLoginKeyRotationValidatorTests
{
Id = Guid.Parse("00000000-0000-0000-0000-000000000002"),
SupportsPrf = true,
EncryptedPublicKey = "TestKey",
EncryptedUserKey = "Test"
EncryptedPublicKey = "TestPublicKey",
EncryptedUserKey = "TestUserKey",
EncryptedPrivateKey = "TestPrivateKey"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data });
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)
.Returns(new List<WebAuthnCredential> { data });
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));
@@ -100,20 +114,24 @@ public class WebAuthnLoginKeyRotationValidatorTests
IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)
{
var guid = Guid.NewGuid();
var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel
{
Id = guid,
EncryptedPublicKey = e.EncryptedPublicKey,
}).ToList();
var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e =>
new WebAuthnLoginRotateKeyRequestModel
{
Id = guid,
EncryptedPublicKey = e.EncryptedPublicKey,
EncryptedUserKey = null
}).ToList();
var data = new WebAuthnCredential
{
Id = guid,
SupportsPrf = true,
EncryptedPublicKey = "TestKey",
EncryptedUserKey = "Test"
EncryptedPublicKey = "TestPublicKey",
EncryptedUserKey = "TestUserKey",
EncryptedPrivateKey = "TestPrivateKey"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data });
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)
.Returns(new List<WebAuthnCredential> { data });
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));
@@ -131,19 +149,21 @@ public class WebAuthnLoginKeyRotationValidatorTests
{
Id = guid,
EncryptedUserKey = e.EncryptedUserKey,
EncryptedPublicKey = null,
}).ToList();
var data = new WebAuthnCredential
{
Id = guid,
SupportsPrf = true,
EncryptedPublicKey = "TestKey",
EncryptedUserKey = "Test"
EncryptedPublicKey = "TestPublicKey",
EncryptedUserKey = "TestUserKey",
EncryptedPrivateKey = "TestPrivateKey"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data });
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)
.Returns(new List<WebAuthnCredential> { data });
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));
}
}

View File

@@ -1,65 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Core.Enums;
using Xunit;
namespace Bit.Api.Test.Models.Request.Accounts;
public class KdfRequestModelTests
{
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 1_000_000, null, null)] // Somewhere in the middle
[InlineData(KdfType.PBKDF2_SHA256, 600_000, null, null)] // Right on the lower boundary
[InlineData(KdfType.PBKDF2_SHA256, 2_000_000, null, null)] // Right on the upper boundary
[InlineData(KdfType.Argon2id, 5, 500, 8)] // Somewhere in the middle
[InlineData(KdfType.Argon2id, 2, 15, 1)] // Right on the lower boundary
[InlineData(KdfType.Argon2id, 10, 1024, 16)] // Right on the upper boundary
public void Validate_IsValid(KdfType kdfType, int? kdfIterations, int? kdfMemory, int? kdfParallelism)
{
var model = new KdfRequestModel
{
Kdf = kdfType,
KdfIterations = kdfIterations,
KdfMemory = kdfMemory,
KdfParallelism = kdfParallelism,
Key = "TEST",
NewMasterPasswordHash = "TEST",
};
var results = Validate(model);
Assert.Empty(results);
}
[Theory]
[InlineData(null, 350_000, null, null, 1)] // Although KdfType is nullable, it's marked as [Required]
[InlineData(KdfType.PBKDF2_SHA256, 500_000, null, null, 1)] // Too few iterations
[InlineData(KdfType.PBKDF2_SHA256, 2_000_001, null, null, 1)] // Too many iterations
[InlineData(KdfType.Argon2id, 0, 30, 8, 1)] // Iterations must be greater than 0
[InlineData(KdfType.Argon2id, 10, 14, 8, 1)] // Too little memory
[InlineData(KdfType.Argon2id, 10, 14, 0, 1)] // Too small of a parallelism value
[InlineData(KdfType.Argon2id, 10, 1025, 8, 1)] // Too much memory
[InlineData(KdfType.Argon2id, 10, 512, 17, 1)] // Too big of a parallelism value
public void Validate_Fails(KdfType? kdfType, int? kdfIterations, int? kdfMemory, int? kdfParallelism, int expectedFailures)
{
var model = new KdfRequestModel
{
Kdf = kdfType,
KdfIterations = kdfIterations,
KdfMemory = kdfMemory,
KdfParallelism = kdfParallelism,
Key = "TEST",
NewMasterPasswordHash = "TEST",
};
var results = Validate(model);
Assert.NotEmpty(results);
Assert.Equal(expectedFailures, results.Count);
}
public static List<ValidationResult> Validate(KdfRequestModel model)
{
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, new ValidationContext(model), results);
return results;
}
}

View File

@@ -4,8 +4,8 @@ using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.PushRegistration;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;

View File

@@ -0,0 +1,121 @@
using Bit.Api.Models.Public.Response;
using Bit.Api.Public.Controllers;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Mvc;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Public.Controllers;
[ControllerCustomize(typeof(CollectionsController))]
[SutProviderCustomize]
public class CollectionsControllerTests
{
[Theory, BitAutoData]
public async Task Get_WithDefaultUserCollection_ReturnsNotFound(
Collection collection, SutProvider<CollectionsController> sutProvider)
{
// Arrange
collection.Type = CollectionType.DefaultUserCollection;
var access = new CollectionAccessDetails
{
Groups = new List<CollectionAccessSelection>(),
Users = new List<CollectionAccessSelection>()
};
sutProvider.GetDependency<ICurrentContext>()
.OrganizationId.Returns(collection.OrganizationId);
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdWithAccessAsync(collection.Id)
.Returns(new Tuple<Collection?, CollectionAccessDetails>(collection, access));
// Act
var result = await sutProvider.Sut.Get(collection.Id);
// Assert
Assert.IsType<NotFoundResult>(result);
}
[Theory, BitAutoData]
public async Task Get_WithSharedCollection_ReturnsCollection(
Collection collection, SutProvider<CollectionsController> sutProvider)
{
// Arrange
collection.Type = CollectionType.SharedCollection;
var access = new CollectionAccessDetails
{
Groups = [],
Users = []
};
sutProvider.GetDependency<ICurrentContext>()
.OrganizationId.Returns(collection.OrganizationId);
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdWithAccessAsync(collection.Id)
.Returns(new Tuple<Collection?, CollectionAccessDetails>(collection, access));
// Act
var result = await sutProvider.Sut.Get(collection.Id);
// Assert
var jsonResult = Assert.IsType<JsonResult>(result);
var response = Assert.IsType<CollectionResponseModel>(jsonResult.Value);
Assert.Equal(collection.Id, response.Id);
}
[Theory, BitAutoData]
public async Task Delete_WithDefaultUserCollection_ReturnsBadRequest(
Collection collection, SutProvider<CollectionsController> sutProvider)
{
// Arrange
collection.Type = CollectionType.DefaultUserCollection;
sutProvider.GetDependency<ICurrentContext>()
.OrganizationId.Returns(collection.OrganizationId);
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(collection.Id)
.Returns(collection);
// Act
var result = await sutProvider.Sut.Delete(collection.Id);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
var errorResponse = Assert.IsType<ErrorResponseModel>(badRequestResult.Value);
Assert.Contains("You cannot delete a collection with the type as DefaultUserCollection", errorResponse.Message);
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive()
.DeleteAsync(Arg.Any<Collection>());
}
[Theory, BitAutoData]
public async Task Delete_WithSharedCollection_ReturnsOk(
Collection collection, SutProvider<CollectionsController> sutProvider)
{
// Arrange
collection.Type = CollectionType.SharedCollection;
sutProvider.GetDependency<ICurrentContext>()
.OrganizationId.Returns(collection.OrganizationId);
sutProvider.GetDependency<ICollectionRepository>()
.GetByIdAsync(collection.Id)
.Returns(collection);
// Act
var result = await sutProvider.Sut.Delete(collection.Id);
// Assert
Assert.IsType<OkResult>(result);
await sutProvider.GetDependency<ICollectionRepository>()
.Received(1)
.DeleteAsync(collection);
}
}

View File

@@ -317,7 +317,7 @@ public class ProjectsControllerTests
[Theory]
[BitAutoData]
public async Task BulkDeleteProjects_ReturnsAccessDeniedForProjectsWithoutAccess_Success(
SutProvider<ProjectsController> sutProvider, List<Project> data)
SutProvider<ProjectsController> sutProvider, Guid userId, List<Project> data)
{
var ids = data.Select(project => project.Id).ToList();
@@ -333,6 +333,7 @@ public class ProjectsControllerTests
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IProjectRepository>().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
var results = await sutProvider.Sut.BulkDeleteAsync(ids);
@@ -346,7 +347,7 @@ public class ProjectsControllerTests
[Theory]
[BitAutoData]
public async Task BulkDeleteProjects_Success(SutProvider<ProjectsController> sutProvider, List<Project> data)
public async Task BulkDeleteProjects_Success(SutProvider<ProjectsController> sutProvider, Guid userId, List<Project> data)
{
var ids = data.Select(project => project.Id).ToList();
var organizationId = data.First().OrganizationId;
@@ -357,7 +358,7 @@ public class ProjectsControllerTests
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), project,
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
}
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IProjectRepository>().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);

View File

@@ -19,6 +19,7 @@ using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;
@@ -152,6 +153,7 @@ public class SecretsControllerTests
SecretCreateRequestModel data, Guid organizationId)
{
data = SetupSecretCreateRequest(sutProvider, data, organizationId);
SetControllerUser(sutProvider, new Guid());
await sutProvider.Sut.CreateAsync(organizationId, data);
@@ -186,6 +188,7 @@ public class SecretsControllerTests
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<SecretAccessPoliciesUpdates>(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Success());
SetControllerUser(sutProvider, new Guid());
await sutProvider.Sut.CreateAsync(organizationId, data);
@@ -199,6 +202,7 @@ public class SecretsControllerTests
SecretUpdateRequestModel data, Secret currentSecret)
{
data = SetupSecretUpdateRequest(data);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Secret>(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
@@ -239,7 +243,7 @@ public class SecretsControllerTests
SecretUpdateRequestModel data, Secret currentSecret)
{
data = SetupSecretUpdateRequest(data);
SetControllerUser(sutProvider, new Guid());
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Secret>(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
@@ -260,7 +264,6 @@ public class SecretsControllerTests
SecretUpdateRequestModel data, Secret currentSecret, SecretAccessPoliciesUpdates accessPoliciesUpdates)
{
data = SetupSecretUpdateAccessPoliciesRequest(sutProvider, data, currentSecret, accessPoliciesUpdates);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<SecretAccessPoliciesUpdates>(),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());
@@ -339,6 +342,8 @@ public class SecretsControllerTests
{
var ids = data.Select(s => s.Id).ToList();
var organizationId = data.First().OrganizationId;
SetControllerUser(sutProvider, new Guid());
foreach (var secret in data)
{
secret.OrganizationId = organizationId;
@@ -378,7 +383,7 @@ public class SecretsControllerTests
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
SetControllerUser(sutProvider, new Guid());
var results = await sutProvider.Sut.BulkDeleteAsync(ids);
await sutProvider.GetDependency<IDeleteSecretCommand>().Received(1)
@@ -434,7 +439,7 @@ public class SecretsControllerTests
{
var (ids, request) = BuildGetSecretsRequestModel(data);
var organizationId = SetOrganizations(ref data);
SetControllerUser(sutProvider, new Guid());
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,
@@ -507,7 +512,7 @@ public class SecretsControllerTests
SutProvider<SecretsController> sutProvider, Guid organizationId)
{
var lastSyncedDate = SetupSecretsSyncRequest(nullLastSyncedDate, secrets, sutProvider, organizationId);
SetControllerUser(sutProvider, new Guid());
var result = await sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate);
Assert.True(result.HasChanges);
Assert.NotNull(result.Secrets);
@@ -610,4 +615,19 @@ public class SecretsControllerTests
.ReturnsForAnyArgs(data.ToSecret(currentSecret));
return data;
}
private static void SetControllerUser(SutProvider<SecretsController> sutProvider, Guid userId)
{
var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, userId.ToString()) };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
sutProvider.Sut.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext
{
HttpContext = new DefaultHttpContext { User = principal }
};
sutProvider.GetDependency<IUserService>().GetProperUserId(principal).Returns(userId);
}
}

View File

@@ -361,7 +361,7 @@ public class ServiceAccountsControllerTests
[Theory]
[BitAutoData]
public async Task BulkDelete_ReturnsAccessDeniedForProjectsWithoutAccess_Success(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data)
public async Task BulkDelete_ReturnsAccessDeniedForProjectsWithoutAccess_Success(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data, Guid userId)
{
var ids = data.Select(sa => sa.Id).ToList();
var organizationId = data.First().OrganizationId;
@@ -377,6 +377,7 @@ public class ServiceAccountsControllerTests
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
var results = await sutProvider.Sut.BulkDeleteAsync(ids);
@@ -390,7 +391,7 @@ public class ServiceAccountsControllerTests
[Theory]
[BitAutoData]
public async Task BulkDelete_Success(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data)
public async Task BulkDelete_Success(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data, Guid userId)
{
var ids = data.Select(sa => sa.Id).ToList();
var organizationId = data.First().OrganizationId;
@@ -404,6 +405,7 @@ public class ServiceAccountsControllerTests
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
var results = await sutProvider.Sut.BulkDeleteAsync(ids);

View File

@@ -20,6 +20,7 @@ using NSubstitute;
using NSubstitute.ClearExtensions;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
using ImportCiphersLimitationSettings = Bit.Core.Settings.GlobalSettings.ImportCiphersLimitationSettings;
namespace Bit.Api.Test.Tools.Controllers;
@@ -27,6 +28,12 @@ namespace Bit.Api.Test.Tools.Controllers;
[SutProviderCustomize]
public class ImportCiphersControllerTests
{
private readonly ImportCiphersLimitationSettings _organizationCiphersLimitations = new()
{
CiphersLimit = 40000,
CollectionRelationshipsLimit = 80000,
CollectionsLimit = 2000
};
/*************************
* PostImport - Individual
@@ -35,7 +42,7 @@ public class ImportCiphersControllerTests
public async Task PostImportIndividual_ImportCiphersRequestModel_BadRequestException(SutProvider<ImportCiphersController> sutProvider, IFixture fixture)
{
// Arrange
sutProvider.GetDependency<Core.Settings.GlobalSettings>()
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
var ciphers = fixture.CreateMany<CipherRequestModel>(7001).ToArray();
var model = new ImportCiphersRequestModel
@@ -90,24 +97,27 @@ public class ImportCiphersControllerTests
****************************/
[Theory, BitAutoData]
public async Task PostImportOrganization_ImportOrganizationCiphersRequestModel_BadRequestException(SutProvider<ImportCiphersController> sutProvider, IFixture fixture)
public async Task PostImportOrganization_ImportOrganizationCiphersRequestModel_BadRequestException(
SutProvider<ImportCiphersController> sutProvider,
IFixture fixture)
{
// Arrange
var globalSettings = sutProvider.GetDependency<Core.Settings.GlobalSettings>();
globalSettings.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
// Limits are set in appsettings.json, making values small for test to run faster.
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = new()
{
CiphersLimit = 4,
CollectionRelationshipsLimit = 8,
CollectionsLimit = 2
};
var userService = sutProvider.GetDependency<Bit.Core.Services.IUserService>();
userService.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(null as Guid?);
globalSettings.ImportCiphersLimitation = new GlobalSettings.ImportCiphersLimitationSettings()
{ // limits are set in appsettings.json, making values small for test to run faster.
CiphersLimit = 200,
CollectionsLimit = 400,
CollectionRelationshipsLimit = 20
};
var ciphers = fixture.CreateMany<CipherRequestModel>(201).ToArray();
var ciphers = fixture.CreateMany<CipherRequestModel>(5).ToArray();
var model = new ImportOrganizationCiphersRequestModel
{
Collections = null,
@@ -116,7 +126,7 @@ public class ImportCiphersControllerTests
};
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostImport(Arg.Any<string>(), model));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostImportOrganization(Arg.Any<string>(), model));
// Assert
Assert.Equal("You cannot import this much data at once.", exception.Message);
@@ -133,7 +143,10 @@ public class ImportCiphersControllerTests
var orgIdGuid = Guid.Parse(orgId);
var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(2).ToArray();
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = _organizationCiphersLimitations;
sutProvider.GetDependency<Bit.Core.Services.IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
@@ -173,7 +186,7 @@ public class ImportCiphersControllerTests
.Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList());
// Act
await sutProvider.Sut.PostImport(orgId, request);
await sutProvider.Sut.PostImportOrganization(orgId, request);
// Assert
await sutProvider.GetDependency<IImportCiphersCommand>()
@@ -196,7 +209,15 @@ public class ImportCiphersControllerTests
var orgIdGuid = Guid.Parse(orgId);
var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(2).ToArray();
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = _organizationCiphersLimitations;
var importCiphersLimitation = new GlobalSettings.ImportCiphersLimitationSettings();
importCiphersLimitation.CiphersLimit = 40000;
importCiphersLimitation.CollectionRelationshipsLimit = 80000;
importCiphersLimitation.CollectionsLimit = 2000;
sutProvider.GetDependency<Bit.Core.Services.IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
@@ -236,7 +257,7 @@ public class ImportCiphersControllerTests
.Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList());
// Act
await sutProvider.Sut.PostImport(orgId, request);
await sutProvider.Sut.PostImportOrganization(orgId, request);
// Assert
await sutProvider.GetDependency<IImportCiphersCommand>()
@@ -259,7 +280,10 @@ public class ImportCiphersControllerTests
var orgIdGuid = Guid.Parse(orgId);
var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(2).ToArray();
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = _organizationCiphersLimitations;
SetupUserService(sutProvider, user);
@@ -300,7 +324,7 @@ public class ImportCiphersControllerTests
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.PostImport(orgId, request));
sutProvider.Sut.PostImportOrganization(orgId, request));
// Assert
Assert.IsType<Bit.Core.Exceptions.BadRequestException>(exception);
@@ -317,7 +341,10 @@ public class ImportCiphersControllerTests
var orgIdGuid = Guid.Parse(orgId);
var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(2).ToArray();
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = _organizationCiphersLimitations;
sutProvider.GetDependency<Bit.Core.Services.IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
@@ -360,7 +387,7 @@ public class ImportCiphersControllerTests
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.PostImport(orgId, request));
sutProvider.Sut.PostImportOrganization(orgId, request));
// Assert
Assert.IsType<Bit.Core.Exceptions.BadRequestException>(exception);
@@ -375,7 +402,10 @@ public class ImportCiphersControllerTests
// Arrange
var orgId = Guid.NewGuid();
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = _organizationCiphersLimitations;
SetupUserService(sutProvider, user);
@@ -427,7 +457,7 @@ public class ImportCiphersControllerTests
// Act
// User imports into collections and creates new collections
// User has ImportCiphers and Create ciphers permission
await sutProvider.Sut.PostImport(orgId.ToString(), request);
await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);
// Assert
await sutProvider.GetDependency<IImportCiphersCommand>()
@@ -448,7 +478,10 @@ public class ImportCiphersControllerTests
// Arrange
var orgId = Guid.NewGuid();
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = _organizationCiphersLimitations;
SetupUserService(sutProvider, user);
@@ -502,7 +535,7 @@ public class ImportCiphersControllerTests
// User has ImportCiphers permission only and doesn't have Create permission
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
{
await sutProvider.Sut.PostImport(orgId.ToString(), request);
await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);
});
// Assert
@@ -525,7 +558,11 @@ public class ImportCiphersControllerTests
// Arrange
var orgId = Guid.NewGuid();
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = _organizationCiphersLimitations;
SetupUserService(sutProvider, user);
// Create new collections
@@ -573,7 +610,7 @@ public class ImportCiphersControllerTests
// Act
// User imports/creates a new collection - existing collections not affected
// User has create permissions and doesn't need import permissions
await sutProvider.Sut.PostImport(orgId.ToString(), request);
await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);
// Assert
await sutProvider.GetDependency<IImportCiphersCommand>()
@@ -594,7 +631,10 @@ public class ImportCiphersControllerTests
// Arrange
var orgId = Guid.NewGuid();
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = _organizationCiphersLimitations;
SetupUserService(sutProvider, user);
@@ -645,7 +685,7 @@ public class ImportCiphersControllerTests
// Act
// User import into existing collection
// User has ImportCiphers permission only and doesn't need create permission
await sutProvider.Sut.PostImport(orgId.ToString(), request);
await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);
// Assert
await sutProvider.GetDependency<IImportCiphersCommand>()
@@ -666,7 +706,10 @@ public class ImportCiphersControllerTests
// Arrange
var orgId = Guid.NewGuid();
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.SelfHosted = false;
sutProvider.GetDependency<GlobalSettings>()
.ImportCiphersLimitation = _organizationCiphersLimitations;
SetupUserService(sutProvider, user);
@@ -710,7 +753,7 @@ public class ImportCiphersControllerTests
// import ciphers only and no collections
// User has Create permissions
// expected to be successful
await sutProvider.Sut.PostImport(orgId.ToString(), request);
await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);
// Assert
await sutProvider.GetDependency<IImportCiphersCommand>()

View File

@@ -1,6 +1,6 @@
using System.Text;
using Bit.Api.Utilities;
using Bit.Core.Models.Business;
using Bit.Core.Billing.Organizations.Models;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;

View File

@@ -0,0 +1,36 @@
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xunit;
namespace Bit.Api.Test.Utilities;
public class KdfSettingsValidatorTests
{
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 1_000_000, null, null)] // Somewhere in the middle
[InlineData(KdfType.PBKDF2_SHA256, 600_000, null, null)] // Right on the lower boundary
[InlineData(KdfType.PBKDF2_SHA256, 2_000_000, null, null)] // Right on the upper boundary
[InlineData(KdfType.Argon2id, 5, 500, 8)] // Somewhere in the middle
[InlineData(KdfType.Argon2id, 2, 15, 1)] // Right on the lower boundary
[InlineData(KdfType.Argon2id, 10, 1024, 16)] // Right on the upper boundary
public void Validate_IsValid(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism);
Assert.Empty(results);
}
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 500_000, null, null, 1)] // Too few iterations
[InlineData(KdfType.PBKDF2_SHA256, 2_000_001, null, null, 1)] // Too many iterations
[InlineData(KdfType.Argon2id, 0, 30, 8, 1)] // Iterations must be greater than 0
[InlineData(KdfType.Argon2id, 10, 14, 8, 1)] // Too little memory
[InlineData(KdfType.Argon2id, 10, 14, 0, 1)] // Too small of a parallelism value
[InlineData(KdfType.Argon2id, 10, 1025, 8, 1)] // Too much memory
[InlineData(KdfType.Argon2id, 10, 512, 17, 1)] // Too big of a parallelism value
public void Validate_Fails(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism, int expectedFailures)
{
var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism);
Assert.NotEmpty(results);
Assert.Equal(expectedFailures, results.Count());
}
}

View File

@@ -1,5 +1,6 @@
using System.Security.Claims;
using System.Text.Json;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Vault.Controllers;
using Bit.Api.Vault.Models;
using Bit.Api.Vault.Models.Request;
@@ -590,11 +591,13 @@ public class CiphersControllerTests
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherOrgDetails.UserId = null;
cipherOrgDetails.OrganizationId = organization.Id;
var cipherDetails = new CipherDetails(cipherOrgDetails);
cipherDetails.Edit = true;
cipherDetails.Manage = true;
@@ -603,7 +606,7 @@ public class CiphersControllerTests
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
@@ -620,7 +623,8 @@ public class CiphersControllerTests
await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(
Arg.Is<CipherDetails>(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);
}
[Theory]
@@ -665,20 +669,20 @@ public class CiphersControllerTests
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_SoftDeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.OrganizationId = organization.Id;
cipherOrgDetails.OrganizationId = organization.Id;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
.Returns(new List<CipherOrganizationDetails> { new() { Id = cipherDetails.Id, OrganizationId = organization.Id } });
.Returns(new List<CipherOrganizationDetails> { new() { Id = cipherOrgDetails.Id, OrganizationId = organization.Id } });
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
@@ -687,74 +691,80 @@ public class CiphersControllerTests
LimitItemDeletion = true
});
await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
await sutProvider.Sut.PutDeleteAdmin(cipherOrgDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(
Arg.Is<CipherDetails>(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_SoftDeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.OrganizationId = organization.Id;
cipherOrgDetails.OrganizationId = organization.Id;
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails });
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
{
Id = organization.Id,
AllowAdminAccessToAllCollectionItems = true
});
await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
await sutProvider.Sut.PutDeleteAdmin(cipherOrgDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(
Arg.Is<CipherDetails>(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);
}
[Theory]
[BitAutoData]
public async Task PutDeleteAdmin_WithCustomUser_WithEditAnyCollectionTrue_SoftDeletesCipher(
CipherDetails cipherDetails, Guid userId,
CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.OrganizationId = organization.Id;
cipherOrgDetails.OrganizationId = organization.Id;
organization.Type = OrganizationUserType.Custom;
organization.Permissions.EditAnyCollection = true;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails });
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });
await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
await sutProvider.Sut.PutDeleteAdmin(cipherOrgDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(
Arg.Is<CipherDetails>(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithEditPermission_WithLimitItemDeletionFalse_SoftDeletesCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherOrgDetails.UserId = null;
cipherOrgDetails.OrganizationId = organization.Id;
var cipherDetails = new CipherDetails(cipherOrgDetails);
cipherDetails.Edit = true;
cipherDetails.Manage = false; // Only Edit permission, not Manage
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails> { cipherDetails });
@@ -768,7 +778,8 @@ public class CiphersControllerTests
await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(
Arg.Is<CipherDetails>(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);
}
[Theory]
@@ -787,7 +798,7 @@ public class CiphersControllerTests
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails> { cipherDetails });
@@ -1061,13 +1072,15 @@ public class CiphersControllerTests
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithManagePermission_RestoresCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Type = CipherType.Login;
cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
cipherOrgDetails.UserId = null;
cipherOrgDetails.OrganizationId = organization.Id;
cipherOrgDetails.Type = CipherType.Login;
cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
var cipherDetails = new CipherDetails(cipherOrgDetails);
cipherDetails.Edit = true;
cipherDetails.Manage = true;
@@ -1076,13 +1089,10 @@ public class CiphersControllerTests
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
{
cipherDetails
});
.Returns(new List<CipherDetails> { cipherDetails });
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
@@ -1091,21 +1101,24 @@ public class CiphersControllerTests
LimitItemDeletion = true
});
var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id);
Assert.IsType<CipherMiniResponseModel>(result);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(Arg.Is<CipherDetails>(
(cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherOrgDetails.UserId = null;
cipherOrgDetails.OrganizationId = organization.Id;
var cipherDetails = new CipherDetails(cipherOrgDetails);
cipherDetails.Edit = true;
cipherDetails.Manage = false;
@@ -1114,13 +1127,10 @@ public class CiphersControllerTests
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails>
{
cipherDetails
});
.Returns(new List<CipherDetails> { cipherDetails });
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
@@ -1136,21 +1146,22 @@ public class CiphersControllerTests
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_RestoresCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Type = CipherType.Login;
cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
cipherOrgDetails.OrganizationId = organization.Id;
cipherOrgDetails.Type = CipherType.Login;
cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)
.Returns(new List<CipherOrganizationDetails> { new() { Id = cipherDetails.Id, OrganizationId = organization.Id } });
.Returns(new List<CipherOrganizationDetails> { new() { Id = cipherOrgDetails.Id, OrganizationId = organization.Id } });
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilityAsync(organization.Id)
.Returns(new OrganizationAbility
@@ -1159,82 +1170,88 @@ public class CiphersControllerTests
LimitItemDeletion = true
});
var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id);
Assert.IsType<CipherMiniResponseModel>(result);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(Arg.Is<CipherDetails>(
(cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_RestoresCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Type = CipherType.Login;
cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
cipherOrgDetails.OrganizationId = organization.Id;
cipherOrgDetails.Type = CipherType.Login;
cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails });
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility
{
Id = organization.Id,
AllowAdminAccessToAllCollectionItems = true
});
var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id);
Assert.IsType<CipherMiniResponseModel>(result);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(Arg.Is<CipherDetails>(
(cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);
}
[Theory]
[BitAutoData]
public async Task PutRestoreAdmin_WithCustomUser_WithEditAnyCollectionTrue_RestoresCipher(
CipherDetails cipherDetails, Guid userId,
CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Type = CipherType.Login;
cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
cipherOrgDetails.OrganizationId = organization.Id;
cipherOrgDetails.Type = CipherType.Login;
cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
organization.Type = OrganizationUserType.Custom;
organization.Permissions.EditAnyCollection = true;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails });
sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });
var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id);
Assert.IsType<CipherMiniResponseModel>(result);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(Arg.Is<CipherDetails>(
(cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_LimitItemDeletionFalse_RestoresCipher(
OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,
OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,
CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)
{
cipherDetails.UserId = null;
cipherDetails.OrganizationId = organization.Id;
cipherDetails.Type = CipherType.Login;
cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
cipherOrgDetails.UserId = null;
cipherOrgDetails.OrganizationId = organization.Id;
cipherOrgDetails.Type = CipherType.Login;
cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData());
var cipherDetails = new CipherDetails(cipherOrgDetails);
cipherDetails.Edit = true;
cipherDetails.Manage = false; // Only Edit permission, not Manage
organization.Type = organizationUserType;
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails> { cipherDetails });
@@ -1249,7 +1266,8 @@ public class CiphersControllerTests
var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);
Assert.IsType<CipherMiniResponseModel>(result);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(cipherDetails, userId, true);
await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(Arg.Is<CipherDetails>(
(cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);
}
[Theory]
@@ -1270,7 +1288,7 @@ public class CiphersControllerTests
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId)
.Returns(new List<CipherDetails> { cipherDetails });
@@ -1319,10 +1337,6 @@ public class CiphersControllerTests
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
@@ -1776,5 +1790,123 @@ public class CiphersControllerTests
);
}
}
[Theory, BitAutoData]
public async Task PostPurge_WhenUserNotFound_ThrowsUnauthorizedAccessException(
SecretVerificationRequestModel model,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns((User)null);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostPurge(model));
}
[Theory, BitAutoData]
public async Task PostPurge_WhenUserVerificationFails_ThrowsBadRequestException(
User user,
SecretVerificationRequestModel model,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<IUserService>()
.VerifySecretAsync(user, model.Secret)
.Returns(false);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostPurge(model));
}
[Theory, BitAutoData]
public async Task PostPurge_UserPurge_WithClaimedUser_ThrowsBadRequestException(
User user,
SecretVerificationRequestModel model,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<IUserService>()
.VerifySecretAsync(user, model.Secret)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.IsClaimedByAnyOrganizationAsync(user.Id)
.Returns(true);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostPurge(model));
}
[Theory, BitAutoData]
public async Task PostPurge_UserPurge_WithUnclaimedUser_Successful(
User user,
SecretVerificationRequestModel model,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<IUserService>()
.VerifySecretAsync(user, model.Secret)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.IsClaimedByAnyOrganizationAsync(user.Id)
.Returns(false);
await sutProvider.Sut.PostPurge(model);
await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.DeleteByUserIdAsync(user.Id);
}
[Theory, BitAutoData]
public async Task PostPurge_OrganizationPurge_WithEditAnyCollectionPermission_Successful(
User user,
SecretVerificationRequestModel model,
Guid organizationId,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<IUserService>()
.VerifySecretAsync(user, model.Secret)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.IsClaimedByAnyOrganizationAsync(user.Id)
.Returns(true);
sutProvider.GetDependency<ICurrentContext>()
.EditAnyCollection(organizationId)
.Returns(true);
await sutProvider.Sut.PostPurge(model, organizationId);
await sutProvider.GetDependency<ICipherService>()
.Received(1)
.PurgeAsync(organizationId);
}
[Theory, BitAutoData]
public async Task PostPurge_OrganizationPurge_WithInsufficientPermissions_ThrowsNotFoundException(
User user,
Guid organizationId,
SecretVerificationRequestModel model,
SutProvider<CiphersController> sutProvider)
{
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<IUserService>()
.VerifySecretAsync(user, model.Secret)
.Returns(true);
sutProvider.GetDependency<IUserService>()
.IsClaimedByAnyOrganizationAsync(user.Id)
.Returns(false);
sutProvider.GetDependency<ICurrentContext>()
.EditAnyCollection(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostPurge(model, organizationId));
}
}

View File

@@ -317,6 +317,55 @@ public class SyncControllerTests
}
}
[Theory]
[BitAutoData]
public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull(
User user, SutProvider<SyncController> sutProvider)
{
user.EquivalentDomains = null;
user.ExcludedGlobalEquivalentDomains = null;
user.MasterPassword = null;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
var result = await sutProvider.Sut.Get();
Assert.Null(result.UserDecryption.MasterPasswordUnlock);
}
[Theory]
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(KdfType.Argon2id, 11, 128, 5)]
public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull(
KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism,
User user, SutProvider<SyncController> sutProvider)
{
user.EquivalentDomains = null;
user.ExcludedGlobalEquivalentDomains = null;
user.Key = "test-key";
user.MasterPassword = "test-master-password";
user.Kdf = kdfType;
user.KdfIterations = kdfIterations;
user.KdfMemory = kdfMemory;
user.KdfParallelism = kdfParallelism;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
var result = await sutProvider.Sut.Get();
Assert.NotNull(result.UserDecryption.MasterPasswordUnlock);
Assert.NotNull(result.UserDecryption.MasterPasswordUnlock.Kdf);
Assert.Equal(kdfType, result.UserDecryption.MasterPasswordUnlock.Kdf.KdfType);
Assert.Equal(kdfIterations, result.UserDecryption.MasterPasswordUnlock.Kdf.Iterations);
Assert.Equal(kdfMemory, result.UserDecryption.MasterPasswordUnlock.Kdf.Memory);
Assert.Equal(kdfParallelism, result.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);
Assert.Equal(user.Key, result.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
Assert.Equal(user.Email.ToLower(), result.UserDecryption.MasterPasswordUnlock.Salt);
}
private async Task AssertMethodsCalledAsync(IUserService userService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,

View File

@@ -8,6 +8,7 @@ using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using NSubstitute.ReceivedExtensions;
@@ -112,7 +113,7 @@ public class FreshdeskControllerTests
[BitAutoData((string)null)]
[BitAutoData(WebhookKey, null)]
public async Task PostWebhookOnyxAi_InvalidWebhookKey_results_in_BadRequest(
string freshdeskWebhookKey, FreshdeskWebhookModel model,
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
BillingSettings billingSettings, SutProvider<FreshdeskController> sutProvider)
{
sutProvider.GetDependency<IOptions<BillingSettings>>()
@@ -126,57 +127,9 @@ public class FreshdeskControllerTests
[Theory]
[BitAutoData(WebhookKey)]
public async Task PostWebhookOnyxAi_invalid_ticketid_results_in_BadRequest(
string freshdeskWebhookKey, FreshdeskWebhookModel model, SutProvider<FreshdeskController> sutProvider)
{
sutProvider.GetDependency<IOptions<BillingSettings>>()
.Value.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
.Returns(mockResponse);
var httpClient = new HttpClient(mockHttpMessageHandler);
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(httpClient);
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
var result = Assert.IsAssignableFrom<BadRequestObjectResult>(response);
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
}
[Theory]
[BitAutoData(WebhookKey)]
public async Task PostWebhookOnyxAi_invalid_freshdesk_response_results_in_BadRequest(
string freshdeskWebhookKey, FreshdeskWebhookModel model,
public async Task PostWebhookOnyxAi_invalid_onyx_response_results_is_logged(
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
SutProvider<FreshdeskController> sutProvider)
{
sutProvider.GetDependency<IOptions<BillingSettings>>()
.Value.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent("non json content. expect json deserializer to throw error")
};
mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
.Returns(mockResponse);
var httpClient = new HttpClient(mockHttpMessageHandler);
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(httpClient);
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
var result = Assert.IsAssignableFrom<BadRequestObjectResult>(response);
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
}
[Theory]
[BitAutoData(WebhookKey)]
public async Task PostWebhookOnyxAi_invalid_onyx_response_results_in_BadRequest(
string freshdeskWebhookKey, FreshdeskWebhookModel model,
FreshdeskViewTicketModel freshdeskTicketInfo, SutProvider<FreshdeskController> sutProvider)
{
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
@@ -184,12 +137,6 @@ public class FreshdeskControllerTests
// mocking freshdesk Api request for ticket info
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var mockFreshdeskResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(freshdeskTicketInfo))
};
mockFreshdeskHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
.Returns(mockFreshdeskResponse);
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
// mocking Onyx api response given a ticket description
@@ -204,34 +151,33 @@ public class FreshdeskControllerTests
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
var result = Assert.IsAssignableFrom<BadRequestObjectResult>(response);
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode);
var _logger = sutProvider.GetDependency<ILogger<FreshdeskController>>();
// workaround because _logger.Received(1).LogWarning(...) does not work
_logger.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Log" && c.GetArguments()[1].ToString().Contains("Error getting answer from Onyx AI"));
// sent call to Onyx API - but we got an error response
_ = mockOnyxHttpMessageHandler.Received(1).Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
// did not call freshdesk to add a note since onyx failed
_ = mockFreshdeskHttpMessageHandler.DidNotReceive().Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
}
[Theory]
[BitAutoData(WebhookKey)]
public async Task PostWebhookOnyxAi_success(
string freshdeskWebhookKey, FreshdeskWebhookModel model,
FreshdeskViewTicketModel freshdeskTicketInfo,
OnyxAnswerWithCitationResponseModel onyxResponse,
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
OnyxResponseModel onyxResponse,
SutProvider<FreshdeskController> sutProvider)
{
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api");
// mocking freshdesk Api request for ticket info (GET)
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var mockFreshdeskResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(freshdeskTicketInfo))
};
mockFreshdeskHttpMessageHandler.Send(
Arg.Is<HttpRequestMessage>(_ => _.Method == HttpMethod.Get),
Arg.Any<CancellationToken>())
.Returns(mockFreshdeskResponse);
// mocking freshdesk api add note request (POST)
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var mockFreshdeskAddNoteResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
mockFreshdeskHttpMessageHandler.Send(
Arg.Is<HttpRequestMessage>(_ => _.Method == HttpMethod.Post),
@@ -239,10 +185,9 @@ public class FreshdeskControllerTests
.Returns(mockFreshdeskAddNoteResponse);
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
// mocking Onyx api response given a ticket description
var mockOnyxHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
onyxResponse.ErrorMsg = string.Empty;
onyxResponse.ErrorMsg = "string.Empty";
var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(onyxResponse))
@@ -260,6 +205,37 @@ public class FreshdeskControllerTests
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
}
[Theory]
[BitAutoData(WebhookKey)]
public async Task PostWebhookOnyxAi_ticket_description_is_empty_return_success(
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
SutProvider<FreshdeskController> sutProvider)
{
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api");
model.TicketDescriptionText = " "; // empty description
// mocking freshdesk api add note request (POST)
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
// mocking Onyx api response given a ticket description
var mockOnyxHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler);
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient);
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("OnyxApi").Returns(onyxHttpClient);
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
var result = Assert.IsAssignableFrom<OkResult>(response);
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
_ = mockFreshdeskHttpMessageHandler.DidNotReceive().Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
_ = mockOnyxHttpMessageHandler.DidNotReceive().Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
}
public class MockHttpMessageHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

View File

@@ -0,0 +1,242 @@
using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Repositories;
using Bit.Core.Services;
using NSubstitute;
using Stripe;
using Xunit;
using Event = Stripe.Event;
namespace Bit.Billing.Test.Services;
public class SetupIntentSucceededHandlerTests
{
private static readonly Event _mockEvent = new() { Id = "evt_test", Type = "setup_intent.succeeded" };
private static readonly string[] _expand = ["payment_method"];
private readonly IOrganizationRepository _organizationRepository;
private readonly IProviderRepository _providerRepository;
private readonly IPushNotificationAdapter _pushNotificationAdapter;
private readonly ISetupIntentCache _setupIntentCache;
private readonly IStripeAdapter _stripeAdapter;
private readonly IStripeEventService _stripeEventService;
private readonly SetupIntentSucceededHandler _handler;
public SetupIntentSucceededHandlerTests()
{
_organizationRepository = Substitute.For<IOrganizationRepository>();
_providerRepository = Substitute.For<IProviderRepository>();
_pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();
_setupIntentCache = Substitute.For<ISetupIntentCache>();
_stripeAdapter = Substitute.For<IStripeAdapter>();
_stripeEventService = Substitute.For<IStripeEventService>();
_handler = new SetupIntentSucceededHandler(
_organizationRepository,
_providerRepository,
_pushNotificationAdapter,
_setupIntentCache,
_stripeAdapter,
_stripeEventService);
}
[Fact]
public async Task HandleAsync_PaymentMethodNotUSBankAccount_Returns()
{
// Arrange
var setupIntent = CreateSetupIntent(hasUSBankAccount: false);
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _setupIntentCache.DidNotReceiveWithAnyArgs().GetSubscriberIdForSetupIntent(Arg.Any<string>());
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
}
[Fact]
public async Task HandleAsync_NoSubscriberIdInCache_Returns()
{
// Arrange
var setupIntent = CreateSetupIntent();
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
.Returns((Guid?)null);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
}
[Fact]
public async Task HandleAsync_ValidOrganization_AttachesPaymentMethodAndSendsNotification()
{
// Arrange
var organizationId = Guid.NewGuid();
var organization = new Organization { Id = organizationId, Name = "Test Org", GatewayCustomerId = "cus_test" };
var setupIntent = CreateSetupIntent();
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
.Returns(organizationId);
_organizationRepository.GetByIdAsync(organizationId)
.Returns(organization);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.Received(1).PaymentMethodAttachAsync(
"pm_test",
Arg.Is<PaymentMethodAttachOptions>(o => o.Customer == organization.GatewayCustomerId));
await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(organization);
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
}
[Fact]
public async Task HandleAsync_ValidProvider_AttachesPaymentMethodAndSendsNotification()
{
// Arrange
var providerId = Guid.NewGuid();
var provider = new Provider { Id = providerId, Name = "Test Provider", GatewayCustomerId = "cus_test" };
var setupIntent = CreateSetupIntent();
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
.Returns(providerId);
_organizationRepository.GetByIdAsync(providerId)
.Returns((Organization?)null);
_providerRepository.GetByIdAsync(providerId)
.Returns(provider);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.Received(1).PaymentMethodAttachAsync(
"pm_test",
Arg.Is<PaymentMethodAttachOptions>(o => o.Customer == provider.GatewayCustomerId));
await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(provider);
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
}
[Fact]
public async Task HandleAsync_OrganizationWithoutGatewayCustomerId_DoesNotAttachPaymentMethod()
{
// Arrange
var organizationId = Guid.NewGuid();
var organization = new Organization { Id = organizationId, Name = "Test Org", GatewayCustomerId = null };
var setupIntent = CreateSetupIntent();
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
.Returns(organizationId);
_organizationRepository.GetByIdAsync(organizationId)
.Returns(organization);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
}
[Fact]
public async Task HandleAsync_ProviderWithoutGatewayCustomerId_DoesNotAttachPaymentMethod()
{
// Arrange
var providerId = Guid.NewGuid();
var provider = new Provider { Id = providerId, Name = "Test Provider", GatewayCustomerId = null };
var setupIntent = CreateSetupIntent();
_stripeEventService.GetSetupIntent(
_mockEvent,
true,
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
.Returns(setupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
.Returns(providerId);
_organizationRepository.GetByIdAsync(providerId)
.Returns((Organization?)null);
_providerRepository.GetByIdAsync(providerId)
.Returns(provider);
// Act
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
}
private static SetupIntent CreateSetupIntent(bool hasUSBankAccount = true)
{
var paymentMethod = new PaymentMethod
{
Id = "pm_test",
Type = "us_bank_account",
UsBankAccount = hasUSBankAccount ? new PaymentMethodUsBankAccount() : null
};
var setupIntent = new SetupIntent
{
Id = "seti_test",
PaymentMethod = paymentMethod
};
return setupIntent;
}
}

View File

@@ -1,8 +1,9 @@
using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Billing.Test.Utilities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Stripe;
using Xunit;
@@ -11,6 +12,9 @@ namespace Bit.Billing.Test.Services;
public class StripeEventServiceTests
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IProviderRepository _providerRepository;
private readonly ISetupIntentCache _setupIntentCache;
private readonly IStripeFacade _stripeFacade;
private readonly StripeEventService _stripeEventService;
@@ -20,8 +24,11 @@ public class StripeEventServiceTests
var baseServiceUriSettings = new GlobalSettings.BaseServiceUriSettings(globalSettings) { CloudRegion = "US" };
globalSettings.BaseServiceUri = baseServiceUriSettings;
_organizationRepository = Substitute.For<IOrganizationRepository>();
_providerRepository = Substitute.For<IProviderRepository>();
_setupIntentCache = Substitute.For<ISetupIntentCache>();
_stripeFacade = Substitute.For<IStripeFacade>();
_stripeEventService = new StripeEventService(globalSettings, Substitute.For<ILogger<StripeEventService>>(), _stripeFacade);
_stripeEventService = new StripeEventService(globalSettings, _organizationRepository, _providerRepository, _setupIntentCache, _stripeFacade);
}
#region GetCharge
@@ -29,50 +36,44 @@ public class StripeEventServiceTests
public async Task GetCharge_EventNotChargeRelated_ThrowsException()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", new Invoice { Id = "in_test" });
// Act
var function = async () => await _stripeEventService.GetCharge(stripeEvent);
// Assert
var exception = await Assert.ThrowsAsync<Exception>(function);
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetCharge(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
Arg.Any<string>(),
Arg.Any<ChargeGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<ChargeGetOptions>());
}
[Fact]
public async Task GetCharge_NotFresh_ReturnsEventCharge()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);
var mockCharge = new Charge { Id = "ch_test", Amount = 1000 };
var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", mockCharge);
// Act
var charge = await _stripeEventService.GetCharge(stripeEvent);
// Assert
Assert.Equivalent(stripeEvent.Data.Object as Charge, charge, true);
Assert.Equal(mockCharge.Id, charge.Id);
Assert.Equal(mockCharge.Amount, charge.Amount);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
Arg.Any<string>(),
Arg.Any<ChargeGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<ChargeGetOptions>());
}
[Fact]
public async Task GetCharge_Fresh_Expand_ReturnsAPICharge()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);
var eventCharge = new Charge { Id = "ch_test", Amount = 1000 };
var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", eventCharge);
var eventCharge = stripeEvent.Data.Object as Charge;
var apiCharge = Copy(eventCharge);
var apiCharge = new Charge { Id = "ch_test", Amount = 2000 };
var expand = new List<string> { "customer" };
@@ -90,9 +91,7 @@ public class StripeEventServiceTests
await _stripeFacade.Received().GetCharge(
apiCharge.Id,
Arg.Is<ChargeGetOptions>(options => options.Expand == expand),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Is<ChargeGetOptions>(options => options.Expand == expand));
}
#endregion
@@ -101,50 +100,44 @@ public class StripeEventServiceTests
public async Task GetCustomer_EventNotCustomerRelated_ThrowsException()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", new Invoice { Id = "in_test" });
// Act
var function = async () => await _stripeEventService.GetCustomer(stripeEvent);
// Assert
var exception = await Assert.ThrowsAsync<Exception>(function);
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetCustomer(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
Arg.Any<string>(),
Arg.Any<CustomerGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<CustomerGetOptions>());
}
[Fact]
public async Task GetCustomer_NotFresh_ReturnsEventCustomer()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var mockCustomer = new Customer { Id = "cus_test", Email = "test@example.com" };
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", mockCustomer);
// Act
var customer = await _stripeEventService.GetCustomer(stripeEvent);
// Assert
Assert.Equivalent(stripeEvent.Data.Object as Customer, customer, true);
Assert.Equal(mockCustomer.Id, customer.Id);
Assert.Equal(mockCustomer.Email, customer.Email);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
Arg.Any<string>(),
Arg.Any<CustomerGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<CustomerGetOptions>());
}
[Fact]
public async Task GetCustomer_Fresh_Expand_ReturnsAPICustomer()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var eventCustomer = new Customer { Id = "cus_test", Email = "test@example.com" };
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", eventCustomer);
var eventCustomer = stripeEvent.Data.Object as Customer;
var apiCustomer = Copy(eventCustomer);
var apiCustomer = new Customer { Id = "cus_test", Email = "updated@example.com" };
var expand = new List<string> { "subscriptions" };
@@ -162,9 +155,7 @@ public class StripeEventServiceTests
await _stripeFacade.Received().GetCustomer(
apiCustomer.Id,
Arg.Is<CustomerGetOptions>(options => options.Expand == expand),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Is<CustomerGetOptions>(options => options.Expand == expand));
}
#endregion
@@ -173,50 +164,44 @@ public class StripeEventServiceTests
public async Task GetInvoice_EventNotInvoiceRelated_ThrowsException()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
// Act
var function = async () => await _stripeEventService.GetInvoice(stripeEvent);
// Assert
var exception = await Assert.ThrowsAsync<Exception>(function);
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetInvoice(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
Arg.Any<string>(),
Arg.Any<InvoiceGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<InvoiceGetOptions>());
}
[Fact]
public async Task GetInvoice_NotFresh_ReturnsEventInvoice()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var mockInvoice = new Invoice { Id = "in_test", AmountDue = 1000 };
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", mockInvoice);
// Act
var invoice = await _stripeEventService.GetInvoice(stripeEvent);
// Assert
Assert.Equivalent(stripeEvent.Data.Object as Invoice, invoice, true);
Assert.Equal(mockInvoice.Id, invoice.Id);
Assert.Equal(mockInvoice.AmountDue, invoice.AmountDue);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
Arg.Any<string>(),
Arg.Any<InvoiceGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<InvoiceGetOptions>());
}
[Fact]
public async Task GetInvoice_Fresh_Expand_ReturnsAPIInvoice()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var eventInvoice = new Invoice { Id = "in_test", AmountDue = 1000 };
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", eventInvoice);
var eventInvoice = stripeEvent.Data.Object as Invoice;
var apiInvoice = Copy(eventInvoice);
var apiInvoice = new Invoice { Id = "in_test", AmountDue = 2000 };
var expand = new List<string> { "customer" };
@@ -234,9 +219,7 @@ public class StripeEventServiceTests
await _stripeFacade.Received().GetInvoice(
apiInvoice.Id,
Arg.Is<InvoiceGetOptions>(options => options.Expand == expand),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Is<InvoiceGetOptions>(options => options.Expand == expand));
}
#endregion
@@ -245,50 +228,44 @@ public class StripeEventServiceTests
public async Task GetPaymentMethod_EventNotPaymentMethodRelated_ThrowsException()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
// Act
var function = async () => await _stripeEventService.GetPaymentMethod(stripeEvent);
// Assert
var exception = await Assert.ThrowsAsync<Exception>(function);
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetPaymentMethod(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
Arg.Any<string>(),
Arg.Any<PaymentMethodGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<PaymentMethodGetOptions>());
}
[Fact]
public async Task GetPaymentMethod_NotFresh_ReturnsEventPaymentMethod()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);
var mockPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" };
var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", mockPaymentMethod);
// Act
var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent);
// Assert
Assert.Equivalent(stripeEvent.Data.Object as PaymentMethod, paymentMethod, true);
Assert.Equal(mockPaymentMethod.Id, paymentMethod.Id);
Assert.Equal(mockPaymentMethod.Type, paymentMethod.Type);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
Arg.Any<string>(),
Arg.Any<PaymentMethodGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<PaymentMethodGetOptions>());
}
[Fact]
public async Task GetPaymentMethod_Fresh_Expand_ReturnsAPIPaymentMethod()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);
var eventPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" };
var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", eventPaymentMethod);
var eventPaymentMethod = stripeEvent.Data.Object as PaymentMethod;
var apiPaymentMethod = Copy(eventPaymentMethod);
var apiPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" };
var expand = new List<string> { "customer" };
@@ -306,9 +283,7 @@ public class StripeEventServiceTests
await _stripeFacade.Received().GetPaymentMethod(
apiPaymentMethod.Id,
Arg.Is<PaymentMethodGetOptions>(options => options.Expand == expand),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Is<PaymentMethodGetOptions>(options => options.Expand == expand));
}
#endregion
@@ -317,50 +292,44 @@ public class StripeEventServiceTests
public async Task GetSubscription_EventNotSubscriptionRelated_ThrowsException()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
// Act
var function = async () => await _stripeEventService.GetSubscription(stripeEvent);
// Assert
var exception = await Assert.ThrowsAsync<Exception>(function);
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetSubscription(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
Arg.Any<string>(),
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task GetSubscription_NotFresh_ReturnsEventSubscription()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var mockSubscription = new Subscription { Id = "sub_test", Status = "active" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
// Act
var subscription = await _stripeEventService.GetSubscription(stripeEvent);
// Assert
Assert.Equivalent(stripeEvent.Data.Object as Subscription, subscription, true);
Assert.Equal(mockSubscription.Id, subscription.Id);
Assert.Equal(mockSubscription.Status, subscription.Status);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
Arg.Any<string>(),
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task GetSubscription_Fresh_Expand_ReturnsAPISubscription()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var eventSubscription = new Subscription { Id = "sub_test", Status = "active" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", eventSubscription);
var eventSubscription = stripeEvent.Data.Object as Subscription;
var apiSubscription = Copy(eventSubscription);
var apiSubscription = new Subscription { Id = "sub_test", Status = "canceled" };
var expand = new List<string> { "customer" };
@@ -378,9 +347,71 @@ public class StripeEventServiceTests
await _stripeFacade.Received().GetSubscription(
apiSubscription.Id,
Arg.Is<SubscriptionGetOptions>(options => options.Expand == expand),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
Arg.Is<SubscriptionGetOptions>(options => options.Expand == expand));
}
#endregion
#region GetSetupIntent
[Fact]
public async Task GetSetupIntent_EventNotSetupIntentRelated_ThrowsException()
{
// Arrange
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
// Act & Assert
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetSetupIntent(stripeEvent));
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(SetupIntent)}'", exception.Message);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent(
Arg.Any<string>(),
Arg.Any<SetupIntentGetOptions>());
}
[Fact]
public async Task GetSetupIntent_NotFresh_ReturnsEventSetupIntent()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test", Status = "succeeded" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
// Act
var setupIntent = await _stripeEventService.GetSetupIntent(stripeEvent);
// Assert
Assert.Equal(mockSetupIntent.Id, setupIntent.Id);
Assert.Equal(mockSetupIntent.Status, setupIntent.Status);
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent(
Arg.Any<string>(),
Arg.Any<SetupIntentGetOptions>());
}
[Fact]
public async Task GetSetupIntent_Fresh_Expand_ReturnsAPISetupIntent()
{
// Arrange
var eventSetupIntent = new SetupIntent { Id = "seti_test", Status = "succeeded" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", eventSetupIntent);
var apiSetupIntent = new SetupIntent { Id = "seti_test", Status = "requires_action" };
var expand = new List<string> { "customer" };
_stripeFacade.GetSetupIntent(
apiSetupIntent.Id,
Arg.Is<SetupIntentGetOptions>(options => options.Expand == expand))
.Returns(apiSetupIntent);
// Act
var setupIntent = await _stripeEventService.GetSetupIntent(stripeEvent, true, expand);
// Assert
Assert.Equal(apiSetupIntent, setupIntent);
Assert.NotSame(eventSetupIntent, setupIntent);
await _stripeFacade.Received().GetSetupIntent(
apiSetupIntent.Id,
Arg.Is<SetupIntentGetOptions>(options => options.Expand == expand));
}
#endregion
@@ -389,18 +420,16 @@ public class StripeEventServiceTests
public async Task ValidateCloudRegion_SubscriptionUpdated_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var mockSubscription = new Subscription { Id = "sub_test" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
var subscription = Copy(stripeEvent.Data.Object as Subscription);
var customer = await GetCustomerAsync();
subscription.Customer = customer;
var customer = CreateMockCustomer();
mockSubscription.Customer = customer;
_stripeFacade.GetSubscription(
subscription.Id,
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
.Returns(mockSubscription);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -409,28 +438,24 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetSubscription(
subscription.Id,
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_ChargeSucceeded_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);
var mockCharge = new Charge { Id = "ch_test" };
var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", mockCharge);
var charge = Copy(stripeEvent.Data.Object as Charge);
var customer = await GetCustomerAsync();
charge.Customer = customer;
var customer = CreateMockCustomer();
mockCharge.Customer = customer;
_stripeFacade.GetCharge(
charge.Id,
mockCharge.Id,
Arg.Any<ChargeGetOptions>())
.Returns(charge);
.Returns(mockCharge);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -439,24 +464,21 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetCharge(
charge.Id,
Arg.Any<ChargeGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockCharge.Id,
Arg.Any<ChargeGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_UpcomingInvoice_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceUpcoming);
var mockInvoice = new Invoice { Id = "in_test", CustomerId = "cus_test" };
var stripeEvent = CreateMockEvent("evt_test", "invoice.upcoming", mockInvoice);
var invoice = Copy(stripeEvent.Data.Object as Invoice);
var customer = await GetCustomerAsync();
var customer = CreateMockCustomer();
_stripeFacade.GetCustomer(
invoice.CustomerId,
mockInvoice.CustomerId,
Arg.Any<CustomerGetOptions>())
.Returns(customer);
@@ -467,28 +489,24 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetCustomer(
invoice.CustomerId,
Arg.Any<CustomerGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockInvoice.CustomerId,
Arg.Any<CustomerGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_InvoiceCreated_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
var mockInvoice = new Invoice { Id = "in_test" };
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", mockInvoice);
var invoice = Copy(stripeEvent.Data.Object as Invoice);
var customer = await GetCustomerAsync();
invoice.Customer = customer;
var customer = CreateMockCustomer();
mockInvoice.Customer = customer;
_stripeFacade.GetInvoice(
invoice.Id,
mockInvoice.Id,
Arg.Any<InvoiceGetOptions>())
.Returns(invoice);
.Returns(mockInvoice);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -497,28 +515,24 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetInvoice(
invoice.Id,
Arg.Any<InvoiceGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockInvoice.Id,
Arg.Any<InvoiceGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_PaymentMethodAttached_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);
var mockPaymentMethod = new PaymentMethod { Id = "pm_test" };
var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", mockPaymentMethod);
var paymentMethod = Copy(stripeEvent.Data.Object as PaymentMethod);
var customer = await GetCustomerAsync();
paymentMethod.Customer = customer;
var customer = CreateMockCustomer();
mockPaymentMethod.Customer = customer;
_stripeFacade.GetPaymentMethod(
paymentMethod.Id,
mockPaymentMethod.Id,
Arg.Any<PaymentMethodGetOptions>())
.Returns(paymentMethod);
.Returns(mockPaymentMethod);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -527,24 +541,21 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetPaymentMethod(
paymentMethod.Id,
Arg.Any<PaymentMethodGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockPaymentMethod.Id,
Arg.Any<PaymentMethodGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_CustomerUpdated_Success()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
var customer = Copy(stripeEvent.Data.Object as Customer);
var mockCustomer = CreateMockCustomer();
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", mockCustomer);
_stripeFacade.GetCustomer(
customer.Id,
mockCustomer.Id,
Arg.Any<CustomerGetOptions>())
.Returns(customer);
.Returns(mockCustomer);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -553,29 +564,24 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetCustomer(
customer.Id,
Arg.Any<CustomerGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockCustomer.Id,
Arg.Any<CustomerGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_MetadataNull_ReturnsFalse()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var mockSubscription = new Subscription { Id = "sub_test" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
var subscription = Copy(stripeEvent.Data.Object as Subscription);
var customer = await GetCustomerAsync();
customer.Metadata = null;
subscription.Customer = customer;
var customer = new Customer { Id = "cus_test", Metadata = null };
mockSubscription.Customer = customer;
_stripeFacade.GetSubscription(
subscription.Id,
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
.Returns(mockSubscription);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -584,29 +590,24 @@ public class StripeEventServiceTests
Assert.False(cloudRegionValid);
await _stripeFacade.Received(1).GetSubscription(
subscription.Id,
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_MetadataNoRegion_DefaultUS_ReturnsTrue()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var mockSubscription = new Subscription { Id = "sub_test" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
var subscription = Copy(stripeEvent.Data.Object as Subscription);
var customer = await GetCustomerAsync();
customer.Metadata = new Dictionary<string, string>();
subscription.Customer = customer;
var customer = new Customer { Id = "cus_test", Metadata = new Dictionary<string, string>() };
mockSubscription.Customer = customer;
_stripeFacade.GetSubscription(
subscription.Id,
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
.Returns(mockSubscription);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -615,32 +616,28 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetSubscription(
subscription.Id,
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_MetadataMiscasedRegion_ReturnsTrue()
public async Task ValidateCloudRegion_MetadataIncorrectlyCasedRegion_ReturnsTrue()
{
// Arrange
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
var mockSubscription = new Subscription { Id = "sub_test" };
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
var subscription = Copy(stripeEvent.Data.Object as Subscription);
var customer = await GetCustomerAsync();
customer.Metadata = new Dictionary<string, string>
var customer = new Customer
{
{ "Region", "US" }
Id = "cus_test",
Metadata = new Dictionary<string, string> { { "Region", "US" } }
};
subscription.Customer = customer;
mockSubscription.Customer = customer;
_stripeFacade.GetSubscription(
subscription.Id,
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
.Returns(mockSubscription);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
@@ -649,31 +646,209 @@ public class StripeEventServiceTests
Assert.True(cloudRegionValid);
await _stripeFacade.Received(1).GetSubscription(
subscription.Id,
Arg.Any<SubscriptionGetOptions>(),
Arg.Any<RequestOptions>(),
Arg.Any<CancellationToken>());
mockSubscription.Id,
Arg.Any<SubscriptionGetOptions>());
}
[Fact]
public async Task ValidateCloudRegion_SetupIntentSucceeded_OrganizationCustomer_Success()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
var organizationId = Guid.NewGuid();
var organizationCustomerId = "cus_org_test";
var mockOrganization = new Core.AdminConsole.Entities.Organization
{
Id = organizationId,
GatewayCustomerId = organizationCustomerId
};
var customer = CreateMockCustomer();
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
.Returns(organizationId);
_organizationRepository.GetByIdAsync(organizationId)
.Returns(mockOrganization);
_stripeFacade.GetCustomer(organizationCustomerId)
.Returns(customer);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
// Assert
Assert.True(cloudRegionValid);
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
await _organizationRepository.Received(1).GetByIdAsync(organizationId);
await _stripeFacade.Received(1).GetCustomer(organizationCustomerId);
}
[Fact]
public async Task ValidateCloudRegion_SetupIntentSucceeded_ProviderCustomer_Success()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
var providerId = Guid.NewGuid();
var providerCustomerId = "cus_provider_test";
var mockProvider = new Core.AdminConsole.Entities.Provider.Provider
{
Id = providerId,
GatewayCustomerId = providerCustomerId
};
var customer = CreateMockCustomer();
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
.Returns(providerId);
_organizationRepository.GetByIdAsync(providerId)
.Returns((Core.AdminConsole.Entities.Organization?)null);
_providerRepository.GetByIdAsync(providerId)
.Returns(mockProvider);
_stripeFacade.GetCustomer(providerCustomerId)
.Returns(customer);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
// Assert
Assert.True(cloudRegionValid);
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
await _organizationRepository.Received(1).GetByIdAsync(providerId);
await _providerRepository.Received(1).GetByIdAsync(providerId);
await _stripeFacade.Received(1).GetCustomer(providerCustomerId);
}
[Fact]
public async Task ValidateCloudRegion_SetupIntentSucceeded_NoSubscriberIdInCache_ReturnsFalse()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
.Returns((Guid?)null);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
// Assert
Assert.False(cloudRegionValid);
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await _providerRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await _stripeFacade.DidNotReceive().GetCustomer(Arg.Any<string>());
}
[Fact]
public async Task ValidateCloudRegion_SetupIntentSucceeded_OrganizationWithoutGatewayCustomerId_ChecksProvider()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
var subscriberId = Guid.NewGuid();
var providerCustomerId = "cus_provider_test";
var mockOrganizationWithoutCustomerId = new Core.AdminConsole.Entities.Organization
{
Id = subscriberId,
GatewayCustomerId = null
};
var mockProvider = new Core.AdminConsole.Entities.Provider.Provider
{
Id = subscriberId,
GatewayCustomerId = providerCustomerId
};
var customer = CreateMockCustomer();
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
.Returns(subscriberId);
_organizationRepository.GetByIdAsync(subscriberId)
.Returns(mockOrganizationWithoutCustomerId);
_providerRepository.GetByIdAsync(subscriberId)
.Returns(mockProvider);
_stripeFacade.GetCustomer(providerCustomerId)
.Returns(customer);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
// Assert
Assert.True(cloudRegionValid);
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
await _organizationRepository.Received(1).GetByIdAsync(subscriberId);
await _providerRepository.Received(1).GetByIdAsync(subscriberId);
await _stripeFacade.Received(1).GetCustomer(providerCustomerId);
}
[Fact]
public async Task ValidateCloudRegion_SetupIntentSucceeded_ProviderWithoutGatewayCustomerId_ReturnsFalse()
{
// Arrange
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
var subscriberId = Guid.NewGuid();
var mockProviderWithoutCustomerId = new Core.AdminConsole.Entities.Provider.Provider
{
Id = subscriberId,
GatewayCustomerId = null
};
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
.Returns(subscriberId);
_organizationRepository.GetByIdAsync(subscriberId)
.Returns((Core.AdminConsole.Entities.Organization?)null);
_providerRepository.GetByIdAsync(subscriberId)
.Returns(mockProviderWithoutCustomerId);
// Act
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
// Assert
Assert.False(cloudRegionValid);
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
await _organizationRepository.Received(1).GetByIdAsync(subscriberId);
await _providerRepository.Received(1).GetByIdAsync(subscriberId);
await _stripeFacade.DidNotReceive().GetCustomer(Arg.Any<string>());
}
#endregion
private static T Copy<T>(T input)
private static Event CreateMockEvent<T>(string id, string type, T dataObject) where T : IStripeEntity
{
var copy = (T)Activator.CreateInstance(typeof(T));
var properties = input.GetType().GetProperties();
foreach (var property in properties)
return new Event
{
var value = property.GetValue(input);
copy!
.GetType()
.GetProperty(property.Name)!
.SetValue(copy, value);
}
return copy;
Id = id,
Type = type,
Data = new EventData
{
Object = (IHasObject)dataObject
}
};
}
private static async Task<Customer> GetCustomerAsync()
=> (await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated)).Data.Object as Customer;
private static Customer CreateMockCustomer()
{
return new Customer
{
Id = "cus_test",
Metadata = new Dictionary<string, string> { { "region", "US" } }
};
}
}

View File

@@ -1,17 +1,22 @@
using Bit.Billing.Constants;
using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Pricing;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Quartz;
using Stripe;
using Xunit;
@@ -27,13 +32,15 @@ public class SubscriptionUpdatedHandlerTests
private readonly IStripeFacade _stripeFacade;
private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand;
private readonly IUserService _userService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationRepository _organizationRepository;
private readonly ISchedulerFactory _schedulerFactory;
private readonly IOrganizationEnableCommand _organizationEnableCommand;
private readonly IOrganizationDisableCommand _organizationDisableCommand;
private readonly IPricingClient _pricingClient;
private readonly IFeatureService _featureService;
private readonly IProviderRepository _providerRepository;
private readonly IProviderService _providerService;
private readonly IScheduler _scheduler;
private readonly IPushNotificationAdapter _pushNotificationAdapter;
private readonly SubscriptionUpdatedHandler _sut;
public SubscriptionUpdatedHandlerTests()
@@ -44,15 +51,20 @@ public class SubscriptionUpdatedHandlerTests
_stripeFacade = Substitute.For<IStripeFacade>();
_organizationSponsorshipRenewCommand = Substitute.For<IOrganizationSponsorshipRenewCommand>();
_userService = Substitute.For<IUserService>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_providerService = Substitute.For<IProviderService>();
_organizationRepository = Substitute.For<IOrganizationRepository>();
_schedulerFactory = Substitute.For<ISchedulerFactory>();
var schedulerFactory = Substitute.For<ISchedulerFactory>();
_organizationEnableCommand = Substitute.For<IOrganizationEnableCommand>();
_organizationDisableCommand = Substitute.For<IOrganizationDisableCommand>();
_pricingClient = Substitute.For<IPricingClient>();
_featureService = Substitute.For<IFeatureService>();
_providerRepository = Substitute.For<IProviderRepository>();
_providerService = Substitute.For<IProviderService>();
var logger = Substitute.For<ILogger<SubscriptionUpdatedHandler>>();
_scheduler = Substitute.For<IScheduler>();
_pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();
_schedulerFactory.GetScheduler().Returns(_scheduler);
schedulerFactory.GetScheduler().Returns(_scheduler);
_sut = new SubscriptionUpdatedHandler(
_stripeEventService,
@@ -61,12 +73,16 @@ public class SubscriptionUpdatedHandlerTests
_stripeFacade,
_organizationSponsorshipRenewCommand,
_userService,
_pushNotificationService,
_organizationRepository,
_schedulerFactory,
schedulerFactory,
_organizationEnableCommand,
_organizationDisableCommand,
_pricingClient);
_pricingClient,
_featureService,
_providerRepository,
_providerService,
logger,
_pushNotificationAdapter);
}
[Fact]
@@ -104,6 +120,339 @@ public class SubscriptionUpdatedHandlerTests
Arg.Is<ITrigger>(t => t.Key.Name == $"cancel-trigger-{subscriptionId}"));
}
[Fact]
public async Task
HandleAsync_UnpaidProviderSubscription_WithManualSuspensionViaMetadata_DisablesProviderAndSchedulesCancellation()
{
// Arrange
var providerId = Guid.NewGuid();
var subscriptionId = "sub_test123";
var previousSubscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Active,
Metadata = new Dictionary<string, string>
{
["suspend_provider"] = null // This is the key part - metadata exists, but value is null
}
};
var currentSubscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30),
Metadata = new Dictionary<string, string>
{
["providerId"] = providerId.ToString(),
["suspend_provider"] = "true" // Now has a value, indicating manual suspension
},
TestClock = null
};
var parsedEvent = new Event
{
Id = "evt_test123",
Type = HandledStripeWebhook.SubscriptionUpdated,
Data = new EventData
{
Object = currentSubscription,
PreviousAttributes = JObject.FromObject(previousSubscription)
}
};
var provider = new Provider { Id = providerId, Enabled = true };
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover).Returns(true);
_stripeEventService.GetSubscription(parsedEvent, true, Arg.Any<List<string>>()).Returns(currentSubscription);
_stripeEventUtilityService.GetIdsFromMetadata(currentSubscription.Metadata)
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository.GetByIdAsync(providerId).Returns(provider);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
Assert.False(provider.Enabled);
await _providerService.Received(1).UpdateAsync(provider);
// Verify that UpdateSubscription was called with both CancelAt and the new metadata
await _stripeFacade.Received(1).UpdateSubscription(
subscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options =>
options.CancelAt.HasValue &&
options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&
options.Metadata != null &&
options.Metadata.ContainsKey("suspended_provider_via_webhook_at")));
}
[Fact]
public async Task
HandleAsync_UnpaidProviderSubscription_WithValidTransition_DisablesProviderAndSchedulesCancellation()
{
// Arrange
var providerId = Guid.NewGuid();
var subscriptionId = "sub_test123";
var previousSubscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Active,
Metadata = new Dictionary<string, string> { ["providerId"] = providerId.ToString() }
};
var currentSubscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30),
Metadata = new Dictionary<string, string> { ["providerId"] = providerId.ToString() },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" },
TestClock = null
};
var parsedEvent = new Event
{
Id = "evt_test123",
Type = HandledStripeWebhook.SubscriptionUpdated,
Data = new EventData
{
Object = currentSubscription,
PreviousAttributes = JObject.FromObject(previousSubscription)
}
};
var provider = new Provider { Id = providerId, Enabled = true };
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover).Returns(true);
_stripeEventService.GetSubscription(parsedEvent, true, Arg.Any<List<string>>()).Returns(currentSubscription);
_stripeEventUtilityService.GetIdsFromMetadata(currentSubscription.Metadata)
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository.GetByIdAsync(providerId).Returns(provider);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
Assert.False(provider.Enabled);
await _providerService.Received(1).UpdateAsync(provider);
// Verify that UpdateSubscription was called with CancelAt but WITHOUT suspension metadata
await _stripeFacade.Received(1).UpdateSubscription(
subscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options =>
options.CancelAt.HasValue &&
options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&
(options.Metadata == null || !options.Metadata.ContainsKey("suspended_provider_via_webhook_at"))));
}
[Fact]
public async Task HandleAsync_UnpaidProviderSubscription_WithoutValidTransition_DisablesProviderOnly()
{
// Arrange
var providerId = Guid.NewGuid();
const string subscriptionId = "sub_123";
var subscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
};
var provider = new Provider { Id = providerId, Name = "Test Provider", Enabled = true };
var parsedEvent = new Event
{
Data = new EventData
{
PreviousAttributes = JObject.FromObject(new
{
status = "unpaid" // No valid transition
})
}
};
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
_providerRepository.GetByIdAsync(providerId)
.Returns(provider);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
Assert.False(provider.Enabled);
await _providerService.Received(1).UpdateAsync(provider);
await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
}
[Fact]
public async Task HandleAsync_UnpaidProviderSubscription_WithNoPreviousAttributes_DisablesProviderOnly()
{
// Arrange
var providerId = Guid.NewGuid();
const string subscriptionId = "sub_123";
var subscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
};
var provider = new Provider { Id = providerId, Name = "Test Provider", Enabled = true };
var parsedEvent = new Event { Data = new EventData { PreviousAttributes = null } };
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
_providerRepository.GetByIdAsync(providerId)
.Returns(provider);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
Assert.False(provider.Enabled);
await _providerService.Received(1).UpdateAsync(provider);
await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
}
[Fact]
public async Task HandleAsync_UnpaidProviderSubscription_WithIncompleteExpiredStatus_DisablesProvider()
{
// Arrange
var providerId = Guid.NewGuid();
var subscriptionId = "sub_123";
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
var subscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.IncompleteExpired,
CurrentPeriodEnd = currentPeriodEnd,
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "renewal" }
};
var provider = new Provider { Id = providerId, Name = "Test Provider", Enabled = true };
var parsedEvent = new Event { Data = new EventData() };
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
_providerRepository.GetByIdAsync(providerId)
.Returns(provider);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
Assert.False(provider.Enabled);
await _providerService.Received(1).UpdateAsync(provider);
await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
}
[Fact]
public async Task HandleAsync_UnpaidProviderSubscription_WhenFeatureFlagDisabled_DoesNothing()
{
// Arrange
var providerId = Guid.NewGuid();
var subscriptionId = "sub_123";
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
var subscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
CurrentPeriodEnd = currentPeriodEnd,
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
};
var parsedEvent = new Event { Data = new EventData() };
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(false);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _providerRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());
await _providerService.DidNotReceive().UpdateAsync(Arg.Any<Provider>());
}
[Fact]
public async Task HandleAsync_UnpaidProviderSubscription_WhenProviderNotFound_DoesNothing()
{
// Arrange
var providerId = Guid.NewGuid();
var subscriptionId = "sub_123";
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
var subscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
CurrentPeriodEnd = currentPeriodEnd,
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
};
var parsedEvent = new Event { Data = new EventData() };
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
_providerRepository.GetByIdAsync(providerId)
.Returns((Provider)null);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _providerService.DidNotReceive().UpdateAsync(Arg.Any<Provider>());
await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
}
[Fact]
public async Task HandleAsync_UnpaidUserSubscription_DisablesPremiumAndCancelsSubscription()
{
@@ -119,10 +468,10 @@ public class SubscriptionUpdatedHandlerTests
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } },
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new() { Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId } }
}
Data =
[
new SubscriptionItem { Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId } }
]
}
};
@@ -163,11 +512,7 @@ public class SubscriptionUpdatedHandlerTests
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } }
};
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually2023
};
var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 };
var parsedEvent = new Event { Data = new EventData() };
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
@@ -180,7 +525,7 @@ public class SubscriptionUpdatedHandlerTests
.Returns(organization);
_stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>())
.Returns(new StripeList<Invoice> { Data = new List<Invoice> { new Invoice { Id = "inv_123" } } });
.Returns(new StripeList<Invoice> { Data = [new Invoice { Id = "inv_123" }] });
var plan = new Enterprise2023Plan(true);
_pricingClient.GetPlanOrThrow(organization.PlanType)
@@ -194,8 +539,8 @@ public class SubscriptionUpdatedHandlerTests
.EnableAsync(organizationId);
await _organizationService.Received(1)
.UpdateExpirationDateAsync(organizationId, currentPeriodEnd);
await _pushNotificationService.Received(1)
.PushSyncOrganizationStatusAsync(organization);
await _pushNotificationAdapter.Received(1)
.NotifyEnabledChangedAsync(organization);
}
[Fact]
@@ -262,7 +607,8 @@ public class SubscriptionUpdatedHandlerTests
}
[Fact]
public async Task HandleAsync_WhenSubscriptionIsActive_AndOrganizationHasSecretsManagerTrial_AndRemovingSecretsManagerTrial_RemovesPasswordManagerCoupon()
public async Task
HandleAsync_WhenSubscriptionIsActive_AndOrganizationHasSecretsManagerTrial_AndRemovingSecretsManagerTrial_RemovesPasswordManagerCoupon()
{
// Arrange
var organizationId = Guid.NewGuid();
@@ -274,34 +620,18 @@ public class SubscriptionUpdatedHandlerTests
CustomerId = "cus_123",
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new() { Plan = new Stripe.Plan { Id = "2023-enterprise-org-seat-annually" } }
}
Data = [new SubscriptionItem { Plan = new Plan { Id = "2023-enterprise-org-seat-annually" } }]
},
Customer = new Customer
{
Balance = 0,
Discount = new Discount
{
Coupon = new Coupon { Id = "sm-standalone" }
}
Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } }
},
Discount = new Discount
{
Coupon = new Coupon { Id = "sm-standalone" }
},
Metadata = new Dictionary<string, string>
{
{ "organizationId", organizationId.ToString() }
}
Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } },
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } }
};
var organization = new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually2023
};
var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 };
var plan = new Enterprise2023Plan(true);
_pricingClient.GetPlanOrThrow(organization.PlanType)
@@ -316,20 +646,14 @@ public class SubscriptionUpdatedHandlerTests
{
items = new
{
data = new[]
{
new { plan = new { id = "secrets-manager-enterprise-seat-annually" } }
}
data = new[] { new { plan = new { id = "secrets-manager-enterprise-seat-annually" } } }
},
Items = new StripeList<SubscriptionItem>
{
Data = new List<SubscriptionItem>
{
new SubscriptionItem
{
Plan = new Stripe.Plan { Id = "secrets-manager-enterprise-seat-annually" }
}
}
Data =
[
new SubscriptionItem { Plan = new Plan { Id = "secrets-manager-enterprise-seat-annually" } }
]
}
})
}
@@ -351,4 +675,354 @@ public class SubscriptionUpdatedHandlerTests
await _stripeFacade.Received(1).DeleteCustomerDiscount(subscription.CustomerId);
await _stripeFacade.Received(1).DeleteSubscriptionDiscount(subscription.Id);
}
[Theory]
[MemberData(nameof(GetNonActiveSubscriptions))]
public async Task
HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasNonActive_EnableProviderAndUpdateSubscription(
Subscription previousSubscription)
{
// Arrange
var (providerId, newSubscription, provider, parsedEvent) =
CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);
_stripeEventService
.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_stripeFacade
.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
.Returns(newSubscription);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository
.Received(1)
.GetByIdAsync(providerId);
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.Received(1)
.UpdateSubscription(newSubscription.Id,
Arg.Is<SubscriptionUpdateOptions>(options => options.CancelAtPeriodEnd == false));
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
public async Task
HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasCanceled_EnableProvider()
{
// Arrange
var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Canceled };
var (providerId, newSubscription, provider, parsedEvent) =
CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);
_stripeEventService
.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository.Received(1).GetByIdAsync(providerId);
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
public async Task
HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasAlreadyActive_EnableProvider()
{
// Arrange
var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Active };
var (providerId, newSubscription, provider, parsedEvent) =
CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);
_stripeEventService
.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository.Received(1).GetByIdAsync(providerId);
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
public async Task
HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasTrailing_EnableProvider()
{
// Arrange
var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Trialing };
var (providerId, newSubscription, provider, parsedEvent) =
CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);
_stripeEventService
.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository.Received(1).GetByIdAsync(providerId);
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
public async Task
HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasPastDue_EnableProvider()
{
// Arrange
var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.PastDue };
var (providerId, newSubscription, provider, parsedEvent) =
CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);
_stripeEventService
.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository
.Received(1)
.GetByIdAsync(Arg.Any<Guid>());
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
public async Task HandleAsync_ActiveProviderSubscriptionEvent_AndProviderDoesNotExist_NoChanges()
{
// Arrange
var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Unpaid };
var (providerId, newSubscription, _, parsedEvent) =
CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);
_stripeEventService
.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository
.Received(1)
.GetByIdAsync(providerId);
await _providerService
.DidNotReceive()
.UpdateAsync(Arg.Any<Provider>());
await _stripeFacade
.DidNotReceive()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
public async Task HandleAsync_ActiveProviderSubscriptionEvent_WithNoPreviousAttributes_EnableProvider()
{
// Arrange
var (providerId, newSubscription, provider, parsedEvent) =
CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(null);
_stripeEventService
.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository
.Received(1)
.GetByIdAsync(Arg.Any<Guid>());
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.DidNotReceive()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
private static (Guid providerId, Subscription newSubscription, Provider provider, Event parsedEvent)
CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(Subscription? previousSubscription)
{
var providerId = Guid.NewGuid();
var newSubscription = new Subscription
{
Id = previousSubscription?.Id ?? "sub_123",
Status = StripeSubscriptionStatus.Active,
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } }
};
var provider = new Provider { Id = providerId, Enabled = false };
var parsedEvent = new Event
{
Data = new EventData
{
Object = newSubscription,
PreviousAttributes =
previousSubscription == null ? null : JObject.FromObject(previousSubscription)
}
};
return (providerId, newSubscription, provider, parsedEvent);
}
public static IEnumerable<object[]> GetNonActiveSubscriptions()
{
return new List<object[]>
{
new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Unpaid } },
new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Incomplete } },
new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.IncompleteExpired } },
new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Paused } }
};
}
}

View File

@@ -1,4 +1,7 @@
using System.Reflection;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Reflection;
using AutoFixture;
using Bit.Test.Common.Helpers;
using Xunit;

View File

@@ -1,4 +1,7 @@
using AutoFixture;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using AutoFixture;
using AutoFixture.Xunit2;
namespace Bit.Test.Common.AutoFixture.Attributes;

View File

@@ -1,4 +1,7 @@
using System.Reflection;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Reflection;
using Xunit.Sdk;
namespace Bit.Test.Common.AutoFixture.Attributes;

View File

@@ -1,4 +1,7 @@
using AutoFixture;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using AutoFixture;
using Bit.Test.Common.AutoFixture.JsonDocumentFixtures;
namespace Bit.Test.Common.AutoFixture.Attributes;

View File

@@ -1,4 +1,7 @@
using AutoFixture;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using AutoFixture;
using AutoFixture.Kernel;
namespace Bit.Test.Common.AutoFixture;

View File

@@ -1,4 +1,7 @@
using System.Text.Json;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Text.Json;
using AutoFixture;
using AutoFixture.Kernel;

View File

@@ -1,4 +1,7 @@
using System.Reflection;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Reflection;
using AutoFixture;
using AutoFixture.Kernel;

View File

@@ -1,4 +1,7 @@
using AutoFixture;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using AutoFixture;
using AutoFixture.Kernel;
namespace Bit.Test.Common.AutoFixture.Attributes;

View File

@@ -1,4 +1,7 @@
using AutoFixture;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using AutoFixture;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Time.Testing;

View File

@@ -1,4 +1,7 @@
using Bit.Core.Tokens;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Core.Tokens;
namespace Bit.Test.Common.Fakes;

View File

@@ -1,4 +1,7 @@
using System.Collections;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Collections;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;

View File

@@ -1,4 +1,7 @@
using System.Net;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Net;
namespace Bit.Test.Common.MockedHttpClient;

View File

@@ -1,4 +1,7 @@
using System.Net;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Net;
using System.Net.Http.Headers;
using System.Text;

View File

@@ -1,13 +1,11 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Bit.Core.Models.Mail;
using Bit.Core.Platform.X509ChainCustomization;
using Bit.Core.Services;
using Bit.Core.Settings;
using MailKit.Security;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Rnwood.SmtpServer;
using Rnwood.SmtpServer.Extensions.Auth;
using Xunit.Abstractions;
@@ -104,8 +102,7 @@ public class MailKitSmtpMailDeliveryServiceTests
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(new X509ChainOptions())
NullLogger<MailKitSmtpMailDeliveryService>.Instance
);
await Assert.ThrowsAsync<SslHandshakeException>(
@@ -118,117 +115,6 @@ public class MailKitSmtpMailDeliveryServiceTests
);
}
[Fact]
public async Task SendEmailAsync_SmtpServerUsingSelfSignedCert_CertInCustomLocation_Works()
{
// If an SMTP server is using a self signed cert we will in the future
// allow a custom location for certificates to be stored and the certitifactes
// stored there will also be trusted.
var port = RandomPort();
var behavior = new DefaultServerBehaviour(false, port, _selfSignedCert);
using var smtpServer = new SmtpServer(behavior);
smtpServer.Start();
var globalSettings = GetSettings(gs =>
{
gs.Mail.Smtp.Port = port;
gs.Mail.Smtp.Ssl = true;
});
var x509ChainOptions = new X509ChainOptions
{
AdditionalCustomTrustCertificates =
[
_selfSignedCert,
],
};
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(x509ChainOptions)
);
var tcs = new TaskCompletionSource();
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
cts.Token.Register(() => _ = tcs.TrySetCanceled());
behavior.MessageReceivedEventHandler += (sender, args) =>
{
if (args.Message.Recipients.Contains("test1@example.com"))
{
tcs.SetResult();
}
return Task.CompletedTask;
};
await mailKitDeliveryService.SendEmailAsync(new MailMessage
{
Subject = "Test",
ToEmails = ["test1@example.com"],
TextContent = "Hi",
}, cts.Token);
// Wait for email
await tcs.Task;
}
[Fact]
public async Task SendEmailAsync_SmtpServerUsingSelfSignedCert_CertInCustomLocation_WithUnrelatedCerts_Works()
{
// If an SMTP server is using a self signed cert we will in the future
// allow a custom location for certificates to be stored and the certitifactes
// stored there will also be trusted.
var port = RandomPort();
var behavior = new DefaultServerBehaviour(false, port, _selfSignedCert);
using var smtpServer = new SmtpServer(behavior);
smtpServer.Start();
var globalSettings = GetSettings(gs =>
{
gs.Mail.Smtp.Port = port;
gs.Mail.Smtp.Ssl = true;
});
var x509ChainOptions = new X509ChainOptions
{
AdditionalCustomTrustCertificates =
[
_selfSignedCert,
CreateSelfSignedCert("example.com"),
],
};
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(x509ChainOptions)
);
var tcs = new TaskCompletionSource();
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
cts.Token.Register(() => _ = tcs.TrySetCanceled());
behavior.MessageReceivedEventHandler += (sender, args) =>
{
if (args.Message.Recipients.Contains("test1@example.com"))
{
tcs.SetResult();
}
return Task.CompletedTask;
};
await mailKitDeliveryService.SendEmailAsync(new MailMessage
{
Subject = "Test",
ToEmails = ["test1@example.com"],
TextContent = "Hi",
}, cts.Token);
// Wait for email
await tcs.Task;
}
[Fact]
public async Task SendEmailAsync_Succeeds_WhenCertIsSelfSigned_ServerIsTrusted()
{
@@ -249,8 +135,7 @@ public class MailKitSmtpMailDeliveryServiceTests
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(new X509ChainOptions())
NullLogger<MailKitSmtpMailDeliveryService>.Instance
);
var tcs = new TaskCompletionSource();
@@ -296,8 +181,7 @@ public class MailKitSmtpMailDeliveryServiceTests
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(new X509ChainOptions())
NullLogger<MailKitSmtpMailDeliveryService>.Instance
);
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
@@ -332,8 +216,7 @@ public class MailKitSmtpMailDeliveryServiceTests
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(new X509ChainOptions())
NullLogger<MailKitSmtpMailDeliveryService>.Instance
);
var tcs = new TaskCompletionSource();
@@ -399,8 +282,7 @@ public class MailKitSmtpMailDeliveryServiceTests
var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(
globalSettings,
NullLogger<MailKitSmtpMailDeliveryService>.Instance,
Options.Create(new X509ChainOptions())
NullLogger<MailKitSmtpMailDeliveryService>.Instance
);
var tcs = new TaskCompletionSource();

View File

@@ -0,0 +1,403 @@
using System.Collections.Concurrent;
using Bit.Core.AdminConsole.AbilitiesCache;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.AbilitiesCache;
[SutProviderCustomize]
public class VNextInMemoryApplicationCacheServiceTests
{
[Theory, BitAutoData]
public async Task GetOrganizationAbilitiesAsync_FirstCall_LoadsFromRepository(
ICollection<OrganizationAbility> organizationAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
// Act
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Assert
Assert.IsType<ConcurrentDictionary<Guid, OrganizationAbility>>(result);
Assert.Equal(organizationAbilities.Count, result.Count);
foreach (var ability in organizationAbilities)
{
Assert.True(result.TryGetValue(ability.Id, out var actualAbility));
Assert.Equal(ability, actualAbility);
}
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilitiesAsync_SecondCall_UsesCachedValue(
List<OrganizationAbility> organizationAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
// Act
var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Assert
Assert.Same(firstCall, secondCall);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilityAsync_ExistingId_ReturnsAbility(
List<OrganizationAbility> organizationAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
var targetAbility = organizationAbilities.First();
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
// Act
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(targetAbility.Id);
// Assert
Assert.Equal(targetAbility, result);
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilityAsync_NonExistingId_ReturnsNull(
List<OrganizationAbility> organizationAbilities,
Guid nonExistingId,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
// Act
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(nonExistingId);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public async Task GetProviderAbilitiesAsync_FirstCall_LoadsFromRepository(
List<ProviderAbility> providerAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(providerAbilities);
// Act
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
// Assert
Assert.IsType<ConcurrentDictionary<Guid, ProviderAbility>>(result);
Assert.Equal(providerAbilities.Count, result.Count);
foreach (var ability in providerAbilities)
{
Assert.True(result.TryGetValue(ability.Id, out var actualAbility));
Assert.Equal(ability, actualAbility);
}
await sutProvider.GetDependency<IProviderRepository>().Received(1).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetProviderAbilitiesAsync_SecondCall_UsesCachedValue(
List<ProviderAbility> providerAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(providerAbilities);
// Act
var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
// Assert
Assert.Same(firstCall, secondCall);
await sutProvider.GetDependency<IProviderRepository>().Received(1).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task UpsertOrganizationAbilityAsync_NewOrganization_AddsToCache(
Organization organization,
List<OrganizationAbility> existingAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(existingAbilities);
await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Act
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
// Assert
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
Assert.True(result.ContainsKey(organization.Id));
Assert.Equal(organization.Id, result[organization.Id].Id);
}
[Theory, BitAutoData]
public async Task UpsertOrganizationAbilityAsync_ExistingOrganization_UpdatesCache(
Organization organization,
List<OrganizationAbility> existingAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
existingAbilities.Add(new OrganizationAbility { Id = organization.Id });
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(existingAbilities);
await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Act
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
// Assert
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
Assert.True(result.ContainsKey(organization.Id));
Assert.Equal(organization.Id, result[organization.Id].Id);
}
[Theory, BitAutoData]
public async Task UpsertProviderAbilityAsync_NewProvider_AddsToCache(
Provider provider,
List<ProviderAbility> existingAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(existingAbilities);
await sutProvider.Sut.GetProviderAbilitiesAsync();
// Act
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
// Assert
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
Assert.True(result.ContainsKey(provider.Id));
Assert.Equal(provider.Id, result[provider.Id].Id);
}
[Theory, BitAutoData]
public async Task DeleteOrganizationAbilityAsync_ExistingId_RemovesFromCache(
List<OrganizationAbility> organizationAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
var targetAbility = organizationAbilities.First();
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Act
await sutProvider.Sut.DeleteOrganizationAbilityAsync(targetAbility.Id);
// Assert
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
Assert.False(result.ContainsKey(targetAbility.Id));
}
[Theory, BitAutoData]
public async Task DeleteProviderAbilityAsync_ExistingId_RemovesFromCache(
List<ProviderAbility> providerAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
var targetAbility = providerAbilities.First();
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(providerAbilities);
await sutProvider.Sut.GetProviderAbilitiesAsync();
// Act
await sutProvider.Sut.DeleteProviderAbilityAsync(targetAbility.Id);
// Assert
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
Assert.False(result.ContainsKey(targetAbility.Id));
}
[Theory, BitAutoData]
public async Task ConcurrentAccess_GetOrganizationAbilities_ThreadSafe(
List<OrganizationAbility> organizationAbilities,
SutProvider<VNextInMemoryApplicationCacheService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
var results = new ConcurrentBag<IDictionary<Guid, OrganizationAbility>>();
const int iterationCount = 100;
// Act
await Parallel.ForEachAsync(
Enumerable.Range(0, iterationCount),
async (_, _) =>
{
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
results.Add(result);
});
// Assert
var firstCall = results.First();
Assert.Equal(iterationCount, results.Count);
Assert.All(results, result => Assert.Same(firstCall, result));
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetOrganizationAbilitiesAsync_AfterRefreshInterval_RefreshesFromRepository(
List<OrganizationAbility> organizationAbilities,
List<OrganizationAbility> updatedAbilities)
{
// Arrange
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
.WithFakeTimeProvider()
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities, updatedAbilities);
var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
const int pastIntervalInMinutes = 11;
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes);
// Act
var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Assert
Assert.NotSame(firstCall, secondCall);
Assert.Equal(updatedAbilities.Count, secondCall.Count);
await sutProvider.GetDependency<IOrganizationRepository>().Received(2).GetManyAbilitiesAsync();
}
[Theory, BitAutoData]
public async Task GetProviderAbilitiesAsync_AfterRefreshInterval_RefreshesFromRepository(
List<ProviderAbility> providerAbilities,
List<ProviderAbility> updatedAbilities)
{
// Arrange
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
.WithFakeTimeProvider()
.Create();
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(providerAbilities, updatedAbilities);
var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
const int pastIntervalMinutes = 15;
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalMinutes);
// Act
var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
// Assert
Assert.NotSame(firstCall, secondCall);
Assert.Equal(updatedAbilities.Count, secondCall.Count);
await sutProvider.GetDependency<IProviderRepository>().Received(2).GetManyAbilitiesAsync();
}
public static IEnumerable<object[]> WhenCacheIsWithinIntervalTestCases =>
[
[5, 1],
[10, 1],
];
[Theory]
[BitMemberAutoData(nameof(WhenCacheIsWithinIntervalTestCases))]
public async Task GetOrganizationAbilitiesAsync_WhenCacheIsWithinInterval(
int pastIntervalInMinutes,
int expectCacheHit,
List<OrganizationAbility> organizationAbilities)
{
// Arrange
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
.WithFakeTimeProvider()
.Create();
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyAbilitiesAsync()
.Returns(organizationAbilities);
var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes);
// Act
var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
// Assert
Assert.Same(firstCall, secondCall);
Assert.Equal(organizationAbilities.Count, secondCall.Count);
await sutProvider.GetDependency<IOrganizationRepository>().Received(expectCacheHit).GetManyAbilitiesAsync();
}
[Theory]
[BitMemberAutoData(nameof(WhenCacheIsWithinIntervalTestCases))]
public async Task GetProviderAbilitiesAsync_WhenCacheIsWithinInterval(
int pastIntervalInMinutes,
int expectCacheHit,
List<ProviderAbility> providerAbilities)
{
// Arrange
var sutProvider = new SutProvider<VNextInMemoryApplicationCacheService>()
.WithFakeTimeProvider()
.Create();
sutProvider.GetDependency<IProviderRepository>()
.GetManyAbilitiesAsync()
.Returns(providerAbilities);
var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes);
// Act
var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync();
// Assert
Assert.Same(firstCall, secondCall);
Assert.Equal(providerAbilities.Count, secondCall.Count);
await sutProvider.GetDependency<IProviderRepository>().Received(expectCacheHit).GetManyAbilitiesAsync();
}
private static void SimulateTimeLapseAfterFirstCall(SutProvider<VNextInMemoryApplicationCacheService> sutProvider, int pastIntervalInMinutes) =>
sutProvider
.GetDependency<FakeTimeProvider>()
.Advance(TimeSpan.FromMinutes(pastIntervalInMinutes));
}

View File

@@ -1,81 +0,0 @@
using System.Security.Claims;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Authorization;
[SutProviderCustomize]
public class OrganizationUserUserMiniDetailsAuthorizationHandlerTests
{
[Theory, CurrentContextOrganizationCustomize]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Custom)]
[BitAutoData(OrganizationUserType.User)]
public async Task ReadAll_AnyOrganizationMember_Success(
OrganizationUserType userType,
CurrentContextOrganization organization,
SutProvider<OrganizationUserUserMiniDetailsAuthorizationHandler> sutProvider)
{
organization.Type = userType;
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
var context = new AuthorizationHandlerContext(
new[] { OrganizationUserUserMiniDetailsOperations.ReadAll },
new ClaimsPrincipal(),
new OrganizationScope(organization.Id));
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData, CurrentContextOrganizationCustomize]
public async Task ReadAll_ProviderUser_Success(
CurrentContextOrganization organization,
SutProvider<OrganizationUserUserMiniDetailsAuthorizationHandler> sutProvider)
{
organization.Type = OrganizationUserType.User;
sutProvider.GetDependency<ICurrentContext>()
.GetOrganization(organization.Id)
.Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<ICurrentContext>()
.ProviderUserForOrgAsync(organization.Id)
.Returns(true);
var context = new AuthorizationHandlerContext(
new[] { OrganizationUserUserMiniDetailsOperations.ReadAll },
new ClaimsPrincipal(),
new OrganizationScope(organization.Id));
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData, CurrentContextOrganizationCustomize]
public async Task ReadAll_NotMember_NoSuccess(
CurrentContextOrganization organization,
SutProvider<OrganizationUserUserMiniDetailsAuthorizationHandler> sutProvider)
{
var context = new AuthorizationHandlerContext(
new[] { OrganizationUserUserMiniDetailsOperations.ReadAll },
new ClaimsPrincipal(),
new OrganizationScope(organization.Id)
);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
}

View File

@@ -0,0 +1,35 @@
using System.Reflection;
using AutoFixture;
using AutoFixture.Xunit2;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.Enums;
namespace Bit.Core.Test.AdminConsole.AutoFixture;
internal class OrganizationPolicyDetailsCustomization(
PolicyType policyType,
OrganizationUserType userType,
bool isProvider,
OrganizationUserStatusType userStatus) : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<OrganizationPolicyDetails>(composer => composer
.With(o => o.PolicyType, policyType)
.With(o => o.OrganizationUserType, userType)
.With(o => o.IsProvider, isProvider)
.With(o => o.OrganizationUserStatus, userStatus)
.Without(o => o.PolicyData)); // avoid autogenerating invalid json data
}
}
public class OrganizationPolicyDetailsAttribute(
PolicyType policyType,
OrganizationUserType userType = OrganizationUserType.User,
bool isProvider = false,
OrganizationUserStatusType userStatus = OrganizationUserStatusType.Confirmed) : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
=> new OrganizationPolicyDetailsCustomization(policyType, userType, isProvider, userStatus);
}

View File

@@ -33,3 +33,5 @@ public class PolicyDetailsAttribute(
public override ICustomization GetCustomization(ParameterInfo parameter)
=> new PolicyDetailsCustomization(policyType, userType, isProvider, userStatus);
}

View File

@@ -18,7 +18,7 @@ internal class PolicyUpdateCustomization(PolicyType type, bool enabled) : ICusto
}
}
public class PolicyUpdateAttribute(PolicyType type, bool enabled = true) : CustomizeAttribute
public class PolicyUpdateAttribute(PolicyType type = PolicyType.FreeFamiliesSponsorshipPolicy, bool enabled = true) : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
{

View File

@@ -6,6 +6,23 @@ namespace Bit.Core.Test.AdminConsole.Helpers;
public static class PermissionsHelpers
{
/// <summary>
/// Sets the specified permission.
/// </summary>
/// <param name="permissionName">The permission name specified as a string - using `nameof` is highly recommended.</param>
/// <param name="value">The value to set the permission to.</param>
/// <returns>No value; this mutates the permissions object.</returns>
public static void SetPermission(this Permissions permissions, string permissionName, bool value)
{
var prop = typeof(Permissions).GetProperty(permissionName);
if (prop == null)
{
throw new NullReferenceException("Invalid property name.");
}
prop.SetValue(permissions, true);
}
/// <summary>
/// Return a new Permission object with inverted permissions.
/// This is useful to test negative cases, e.g. "all other permissions should fail".

View File

@@ -14,7 +14,7 @@ public class IntegrationMessageTests
{
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
{
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
MessageId = _messageId,
RetryCount = 2,
RenderedTemplate = string.Empty,
@@ -34,7 +34,7 @@ public class IntegrationMessageTests
{
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
{
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
MessageId = _messageId,
RenderedTemplate = "This is the message",
IntegrationType = IntegrationType.Webhook,

View File

@@ -0,0 +1,91 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Time.Testing;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations;
public class IntegrationOAuthStateTests
{
private readonly FakeTimeProvider _fakeTimeProvider = new(
new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc)
);
[Theory, BitAutoData]
public void FromIntegration_ToString_RoundTripsCorrectly(OrganizationIntegration integration)
{
var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);
var parsed = IntegrationOAuthState.FromString(state.ToString(), _fakeTimeProvider);
Assert.NotNull(parsed);
Assert.Equal(state.IntegrationId, parsed.IntegrationId);
Assert.True(parsed.ValidateOrg(integration.OrganizationId));
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("not-a-valid-state")]
public void FromString_InvalidString_ReturnsNull(string state)
{
var parsed = IntegrationOAuthState.FromString(state, _fakeTimeProvider);
Assert.Null(parsed);
}
[Fact]
public void FromString_InvalidGuid_ReturnsNull()
{
var badState = $"not-a-guid.ABCD1234.1706313600";
var parsed = IntegrationOAuthState.FromString(badState, _fakeTimeProvider);
Assert.Null(parsed);
}
[Theory, BitAutoData]
public void FromString_ExpiredState_ReturnsNull(OrganizationIntegration integration)
{
var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);
// Advance time 30 minutes to exceed the 20-minute max age
_fakeTimeProvider.Advance(TimeSpan.FromMinutes(30));
var parsed = IntegrationOAuthState.FromString(state.ToString(), _fakeTimeProvider);
Assert.Null(parsed);
}
[Theory, BitAutoData]
public void ValidateOrg_WithCorrectOrgId_ReturnsTrue(OrganizationIntegration integration)
{
var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);
Assert.True(state.ValidateOrg(integration.OrganizationId));
}
[Theory, BitAutoData]
public void ValidateOrg_WithWrongOrgId_ReturnsFalse(OrganizationIntegration integration)
{
var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);
Assert.False(state.ValidateOrg(Guid.NewGuid()));
}
[Theory, BitAutoData]
public void ValidateOrg_ModifiedTimestamp_ReturnsFalse(OrganizationIntegration integration)
{
var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);
var parts = state.ToString().Split('.');
parts[2] = $"{_fakeTimeProvider.GetUtcNow().ToUnixTimeSeconds() - 1}";
var modifiedState = IntegrationOAuthState.FromString(string.Join(".", parts), _fakeTimeProvider);
Assert.True(state.ValidateOrg(integration.OrganizationId));
Assert.NotNull(modifiedState);
Assert.False(modifiedState.ValidateOrg(integration.OrganizationId));
}
}

View File

@@ -0,0 +1,102 @@
#nullable enable
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Entities;
using Bit.Core.Models.Data;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations;
public class IntegrationTemplateContextTests
{
[Theory, BitAutoData]
public void EventMessage_ReturnsSerializedJsonOfEvent(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage: eventMessage);
var expected = JsonSerializer.Serialize(eventMessage);
Assert.Equal(expected, sut.EventMessage);
}
[Theory, BitAutoData]
public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, User user)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = user };
Assert.Equal(user.Name, sut.UserName);
}
[Theory, BitAutoData]
public void UserName_WhenUserIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = null };
Assert.Null(sut.UserName);
}
[Theory, BitAutoData]
public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, User user)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = user };
Assert.Equal(user.Email, sut.UserEmail);
}
[Theory, BitAutoData]
public void UserEmail_WhenUserIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = null };
Assert.Null(sut.UserEmail);
}
[Theory, BitAutoData]
public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, User actingUser)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
Assert.Equal(actingUser.Name, sut.ActingUserName);
}
[Theory, BitAutoData]
public void ActingUserName_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null };
Assert.Null(sut.ActingUserName);
}
[Theory, BitAutoData]
public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, User actingUser)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
Assert.Equal(actingUser.Email, sut.ActingUserEmail);
}
[Theory, BitAutoData]
public void ActingUserEmail_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null };
Assert.Null(sut.ActingUserEmail);
}
[Theory, BitAutoData]
public void OrganizationName_WhenOrganizationIsSet_ReturnsDisplayName(EventMessage eventMessage, Organization organization)
{
var sut = new IntegrationTemplateContext(eventMessage) { Organization = organization };
Assert.Equal(organization.DisplayName(), sut.OrganizationName);
}
[Theory, BitAutoData]
public void OrganizationName_WhenOrganizationIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { Organization = null };
Assert.Null(sut.OrganizationName);
}
}

View File

@@ -0,0 +1,20 @@
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class TestListenerConfiguration : IIntegrationListenerConfiguration
{
public string EventQueueName => "event_queue";
public string EventSubscriptionName => "event_subscription";
public string EventTopicName => "event_topic";
public IntegrationType IntegrationType => IntegrationType.Webhook;
public string IntegrationQueueName => "integration_queue";
public string IntegrationRetryQueueName => "integration_retry_queue";
public string IntegrationSubscriptionName => "integration_subscription";
public string IntegrationTopicName => "integration_topic";
public int MaxRetries => 3;
public int EventMaxConcurrentCalls => 1;
public int EventPrefetchCount => 0;
public int IntegrationMaxConcurrentCalls => 1;
public int IntegrationPrefetchCount => 0;
}

View File

@@ -22,6 +22,22 @@ public class OrganizationIntegrationConfigurationDetailsTests
Assert.Equal(expected, result.ToJsonString());
}
[Fact]
public void MergedConfiguration_WithSameKeyIndConfigAndIntegration_GivesPrecedenceToConfiguration()
{
var config = new { config = "A new config value" };
var integration = new { config = "An integration value" };
var expectedObj = new { config = "A new config value" };
var expected = JsonSerializer.Serialize(expectedObj);
var sut = new OrganizationIntegrationConfigurationDetails();
sut.Configuration = JsonSerializer.Serialize(config);
sut.IntegrationConfiguration = JsonSerializer.Serialize(integration);
var result = sut.MergedConfiguration;
Assert.Equal(expected, result.ToJsonString());
}
[Fact]
public void MergedConfiguration_WithInvalidJsonConfigAndIntegration_ReturnsEmptyJson()
{

View File

@@ -5,11 +5,11 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Test.AutoFixture;
using Bit.Core.Test.Billing.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;

View File

@@ -16,7 +16,7 @@ public class InviteOrganizationUsersRequestTests
public void Constructor_WhenPassedInvalidEmail_ThrowsException(string email, OrganizationUserType type, Permissions permissions, string externalId)
{
var exception = Assert.Throws<BadRequestException>(() =>
new OrganizationUserInvite(email, [], [], type, permissions, externalId, false));
new OrganizationUserInviteCommandModel(email, [], [], type, permissions, externalId, false));
Assert.Contains(InvalidEmailErrorMessage, exception.Message);
}
@@ -33,7 +33,7 @@ public class InviteOrganizationUsersRequestTests
};
var exception = Assert.Throws<BadRequestException>(() =>
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: validEmail,
assignedCollections: [invalidCollectionConfiguration],
groups: [],
@@ -51,7 +51,7 @@ public class InviteOrganizationUsersRequestTests
const string validEmail = "test@email.com";
var validCollectionConfiguration = new CollectionAccessSelection { Id = Guid.NewGuid(), Manage = true };
var invite = new OrganizationUserInvite(
var invite = new OrganizationUserInviteCommandModel(
email: validEmail,
assignedCollections: [validCollectionConfiguration],
groups: [],

View File

@@ -156,6 +156,24 @@ public class UpdateGroupCommandTests
() => sutProvider.Sut.UpdateGroupAsync(group, organization, collectionAccess));
}
[Theory, OrganizationCustomize(UseGroups = true), BitAutoData]
public async Task UpdateGroup_WithDefaultUserCollectionType_Throws(SutProvider<UpdateGroupCommand> sutProvider,
Group group, Group oldGroup, Organization organization, List<CollectionAccessSelection> collectionAccess)
{
ArrangeGroup(sutProvider, group, oldGroup);
ArrangeUsers(sutProvider, group);
// Return collections with DefaultUserCollection type
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()
.Select(guid => new Collection { Id = guid, OrganizationId = group.OrganizationId, Type = CollectionType.DefaultUserCollection }).ToList());
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateGroupAsync(group, organization, collectionAccess));
Assert.Contains("You cannot modify group access for collections with the type as DefaultUserCollection.", exception.Message);
}
[Theory, OrganizationCustomize(UseGroups = true), BitAutoData]
public async Task UpdateGroup_MemberBelongsToDifferentOrganization_Throws(SutProvider<UpdateGroupCommand> sutProvider,
Group group, Group oldGroup, Organization organization, IEnumerable<Guid> userAccess)

View File

@@ -0,0 +1,243 @@
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.Import;
using Bit.Core.Auth.Models.Business.Tokenables;
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.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Tokens;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Fakes;
using NSubstitute;
using Xunit;
using Organization = Bit.Core.AdminConsole.Entities.Organization;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Import;
public class ImportOrganizationUsersAndGroupsCommandTests
{
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task OrgImportCallsInviteOrgUserCommand(
SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,
Organization org,
List<OrganizationUserUserDetails> existingUsers,
List<ImportedOrganizationUser> importedUsers,
List<ImportedGroup> newGroups)
{
SetupOrganizationConfigForImport(sutProvider, org, existingUsers, importedUsers);
var orgUsers = new List<OrganizationUser>();
// fix mocked email format, mock OrganizationUsers.
foreach (var u in importedUsers)
{
u.Email += "@bitwardentest.com";
orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId });
}
importedUsers.Add(new ImportedOrganizationUser
{
Email = existingUsers.First().Email,
ExternalId = existingUsers.First().ExternalId
});
existingUsers.First().Type = OrganizationUserType.Owner;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(org).Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers);
sutProvider.GetDependency<IOrganizationRepository>().GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(
new OrganizationSeatCounts
{
Users = existingUsers.Count,
Sponsored = 0
});
sutProvider.GetDependency<IOrganizationService>().InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,
Arg.Any<IEnumerable<(OrganizationUserInvite, string)>>())
.Returns(orgUsers);
await sutProvider.Sut.ImportAsync(org.Id, newGroups, importedUsers, new List<string>(), false);
var expectedNewUsersCount = importedUsers.Count - 1;
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.UpsertManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => !users.Any()));
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.CreateAsync(default);
// Send Invites
await sutProvider.GetDependency<IOrganizationService>().Received(1).
InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,
Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites => invites.Count() == expectedNewUsersCount));
// Send events
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUserUserDetails, EventType, EventSystemUser, DateTime?)>>());
}
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task OverwriteExistingUsers_WhenRemovingUserWithoutMasterPassword_Throws(
SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,
Organization org, List<OrganizationUserUserDetails> existingUsers)
{
SetupOrganizationConfigForImport(sutProvider, org, existingUsers, []);
// Existing user does not have a master password
existingUsers.First().HasMasterPassword = false;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers);
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.ImportAsync(org.Id, [], [], [], true));
Assert.Contains("Sync failed. To proceed, disable the 'Remove and re-add users during next sync' setting and try again.", exception.Message);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.UpsertManyAsync(default);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.CreateAsync(default);
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs()
.InviteUsersAsync(default, default, default, default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUserUserDetails, EventType, EventSystemUser, DateTime?)>>());
}
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task OrgImportCreateNewUsersAndMarryExistingUser(
SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,
Organization org,
List<OrganizationUserUserDetails> existingUsers,
List<ImportedOrganizationUser> importedUsers,
List<ImportedGroup> newGroups)
{
SetupOrganizationConfigForImport(sutProvider, org, existingUsers, importedUsers);
var orgUsers = new List<OrganizationUser>();
var reInvitedUser = existingUsers.First();
// Existing user has no external ID. This will make the SUT call UpsertManyAsync
reInvitedUser.ExternalId = "";
// Mock an existing org user for this "existing" user
var reInvitedOrgUser = new OrganizationUser { Email = reInvitedUser.Email, Id = reInvitedUser.Id };
// fix email formatting, mock orgUsers to be returned
foreach (var u in existingUsers)
{
u.Email += "@bitwardentest.com";
orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId });
}
foreach (var u in importedUsers)
{
u.Email += "@bitwardentest.com";
orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId });
}
// add the existing user to be re-imported
importedUsers.Add(new ImportedOrganizationUser
{
Email = reInvitedUser.Email,
ExternalId = reInvitedUser.Email,
});
var expectedNewUsersCount = importedUsers.Count - 1;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser>([reInvitedOrgUser]));
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers);
sutProvider.GetDependency<IOrganizationRepository>().GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(
new OrganizationSeatCounts
{
Users = existingUsers.Count,
Sponsored = 0
});
sutProvider.GetDependency<IOrganizationService>().InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,
Arg.Any<IEnumerable<(OrganizationUserInvite, string)>>())
.Returns(orgUsers);
await sutProvider.Sut.ImportAsync(org.Id, newGroups, importedUsers, new List<string>(), false);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.CreateAsync(default);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
.CreateAsync(default, default);
// Upserted existing user
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.UpsertManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => users.Count() == 1 && users.First() == reInvitedOrgUser));
// Send Invites
await sutProvider.GetDependency<IOrganizationService>().Received(1).
InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,
Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites => invites.Count() == expectedNewUsersCount));
// Send events
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUserUserDetails, EventType, EventSystemUser, DateTime?)>>());
}
private void SetupOrganizationConfigForImport(
SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,
Organization org,
List<OrganizationUserUserDetails> existingUsers,
List<ImportedOrganizationUser> importedUsers)
{
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
org.UseDirectory = true;
org.Seats = importedUsers.Count + existingUsers.Count + 1;
}
// Must set real guids in order for dictionary of guids to not throw aggregate exceptions
private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository)
{
organizationUserRepository.CreateManyAsync(Arg.Any<IEnumerable<OrganizationUser>>()).Returns(
info =>
{
var orgUsers = info.Arg<IEnumerable<OrganizationUser>>();
foreach (var orgUser in orgUsers)
{
orgUser.Id = Guid.NewGuid();
}
return Task.FromResult<ICollection<Guid>>(orgUsers.Select(u => u.Id).ToList());
}
);
organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>(), Arg.Any<IEnumerable<CollectionAccessSelection>>()).Returns(
info =>
{
var orgUser = info.Arg<OrganizationUser>();
orgUser.Id = Guid.NewGuid();
return Task.FromResult<Guid>(orgUser.Id);
}
);
}
}

View File

@@ -12,6 +12,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AdminConsole.AutoFixture;
@@ -118,6 +119,11 @@ public class ConfirmOrganizationUserCommandTests
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var userRepository = sutProvider.GetDependency<IUserRepository>();
var device = new Device() { Id = Guid.NewGuid(), UserId = user.Id, PushToken = "pushToken", Identifier = "identifier" };
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(user.Id)
.Returns([device]);
org.PlanType = planType;
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
orgUser.UserId = user.Id;
@@ -133,6 +139,12 @@ public class ConfirmOrganizationUserCommandTests
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email);
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
await sutProvider.GetDependency<IPushRegistrationService>()
.Received(1)
.DeleteUserRegistrationOrganizationAsync(
Arg.Is<IEnumerable<string>>(ids => ids.Contains(device.Id.ToString()) && ids.Count() == 1),
org.Id.ToString());
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncOrgKeysAsync(user.Id);
}
@@ -460,25 +472,32 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
var policyDetails = new PolicyDetails
{
OrganizationId = organization.Id,
OrganizationUserId = orgUser.Id,
IsProvider = false,
OrganizationUserStatus = orgUser.Status,
OrganizationUserType = orgUser.Type,
PolicyType = PolicyType.OrganizationDataOwnership
};
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(user.Id)
.Returns(new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Enabled,
[organization.Id]));
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails]));
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
await sutProvider.GetDependency<ICollectionRepository>()
.Received(1)
.CreateAsync(
Arg.Is<Collection>(c => c.Name == collectionName &&
Arg.Is<Collection>(c =>
c.Name == collectionName &&
c.OrganizationId == organization.Id &&
c.Type == CollectionType.DefaultUserCollection),
Arg.Is<IEnumerable<CollectionAccessSelection>>(groups => groups == null),
Arg.Is<IEnumerable<CollectionAccessSelection>>(u =>
u.Count() == 1 &&
u.First().Id == orgUser.Id &&
u.First().Manage == true));
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
Arg.Is<IEnumerable<CollectionAccessSelection>>(cu =>
cu.Single().Id == orgUser.Id &&
cu.Single().Manage));
}
[Theory, BitAutoData]
@@ -497,23 +516,17 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(user.Id)
.Returns(new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Enabled,
[org.Id]));
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, "");
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive()
.CreateAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(), Arg.Any<IEnumerable<CollectionAccessSelection>>());
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(
Organization org, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
[OrganizationUser(OrganizationUserStatusType.Accepted, OrganizationUserType.Owner)] OrganizationUser orgUser, User user,
string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
{
org.PlanType = PlanType.EnterpriseAnnually;
@@ -525,16 +538,23 @@ public class ConfirmOrganizationUserCommandTests
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
var policyDetails = new PolicyDetails
{
OrganizationId = org.Id,
OrganizationUserId = orgUser.Id,
IsProvider = false,
OrganizationUserStatus = orgUser.Status,
OrganizationUserType = orgUser.Type,
PolicyType = PolicyType.OrganizationDataOwnership
};
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(user.Id)
.Returns(new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Enabled,
[Guid.NewGuid()]));
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, [policyDetails]));
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive()
.CreateAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(), Arg.Any<IEnumerable<CollectionAccessSelection>>());
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
}
}

View File

@@ -0,0 +1,467 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
[SutProviderCustomize]
public class DeleteClaimedOrganizationUserAccountCommandTests
{
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithValidSingleUser_CallsDeleteManyUsersAsync(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
organizationUser.OrganizationId = organizationId;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var validationResult = CreateSuccessfulValidationResult(request);
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { organizationUser },
[user],
organizationId,
new Dictionary<Guid, bool> { { organizationUser.Id, true } });
SetupValidatorMock(sutProvider, [validationResult]);
var result = await sutProvider.Sut.DeleteUserAsync(organizationId, organizationUser.Id, deletingUserId);
Assert.Equal(organizationUser.Id, result.Id);
Assert.True(result.Result.IsSuccess);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)));
await AssertSuccessfulUserOperations(sutProvider, [user], [organizationUser]);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithEmptyUserIds_ReturnsEmptyResults(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
Guid organizationId,
Guid deletingUserId)
{
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [], deletingUserId);
Assert.Empty(results);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user1,
User user2,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser1,
[OrganizationUser] OrganizationUser orgUser2)
{
// Arrange
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
orgUser1.UserId = user1.Id;
orgUser2.UserId = user2.Id;
var request1 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser1.Id,
OrganizationUser = orgUser1,
User = user1,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var request2 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser2.Id,
OrganizationUser = orgUser2,
User = user2,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var validationResults = new[]
{
CreateSuccessfulValidationResult(request1),
CreateSuccessfulValidationResult(request2)
};
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { orgUser1, orgUser2 },
[user1, user2],
organizationId,
new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, true } });
SetupValidatorMock(sutProvider, validationResults);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUser1.Id, orgUser2.Id], deletingUserId);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
Assert.All(resultsList, result => Assert.True(result.Result.IsSuccess));
await AssertSuccessfulUserOperations(sutProvider, [user1, user2], [orgUser1, orgUser2]);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithValidationErrors_ReturnsErrorResults(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
Guid organizationId,
Guid orgUserId1,
Guid orgUserId2,
Guid deletingUserId)
{
// Arrange
var request1 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUserId1,
DeletingUserId = deletingUserId
};
var request2 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUserId2,
DeletingUserId = deletingUserId
};
var validationResults = new[]
{
CreateFailedValidationResult(request1, new UserNotClaimedError()),
CreateFailedValidationResult(request2, new InvalidUserStatusError())
};
SetupRepositoryMocks(sutProvider, [], [], organizationId, new Dictionary<Guid, bool>());
SetupValidatorMock(sutProvider, validationResults);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserId1, orgUserId2], deletingUserId);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
Assert.Equal(orgUserId1, resultsList[0].Id);
Assert.True(resultsList[0].Result.IsError);
Assert.IsType<UserNotClaimedError>(resultsList[0].Result.AsError);
Assert.Equal(orgUserId2, resultsList[1].Id);
Assert.True(resultsList[1].Result.IsError);
Assert.IsType<InvalidUserStatusError>(resultsList[1].Result.AsError);
await AssertNoUserOperations(sutProvider);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithMixedValidationResults_HandlesPartialSuccessCorrectly(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User validUser,
Guid organizationId,
Guid validOrgUserId,
Guid invalidOrgUserId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser validOrgUser)
{
validOrgUser.Id = validOrgUserId;
validOrgUser.UserId = validUser.Id;
validOrgUser.OrganizationId = organizationId;
var validRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = validOrgUserId,
OrganizationUser = validOrgUser,
User = validUser,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var invalidRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = invalidOrgUserId,
DeletingUserId = deletingUserId
};
var validationResults = new[]
{
CreateSuccessfulValidationResult(validRequest),
CreateFailedValidationResult(invalidRequest, new UserNotFoundError())
};
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { validOrgUser },
[validUser],
organizationId,
new Dictionary<Guid, bool> { { validOrgUserId, true } });
SetupValidatorMock(sutProvider, validationResults);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [validOrgUserId, invalidOrgUserId], deletingUserId);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
var validResult = resultsList.First(r => r.Id == validOrgUserId);
var invalidResult = resultsList.First(r => r.Id == invalidOrgUserId);
Assert.True(validResult.Result.IsSuccess);
Assert.True(invalidResult.Result.IsError);
Assert.IsType<UserNotFoundError>(invalidResult.Result.AsError);
await AssertSuccessfulUserOperations(sutProvider, [validUser], [validOrgUser]);
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_CancelPremiumsAsync_HandlesGatewayExceptionAndLogsWarning(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser)
{
orgUser.UserId = user.Id;
orgUser.OrganizationId = organizationId;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser.Id,
OrganizationUser = orgUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var validationResult = CreateSuccessfulValidationResult(request);
SetupRepositoryMocks(sutProvider,
new List<OrganizationUser> { orgUser },
[user],
organizationId,
new Dictionary<Guid, bool> { { orgUser.Id, true } });
SetupValidatorMock(sutProvider, [validationResult]);
var gatewayException = new GatewayException("Payment gateway error");
sutProvider.GetDependency<IUserService>()
.CancelPremiumAsync(user)
.ThrowsAsync(gatewayException);
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUser.Id], deletingUserId);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList.First().Result.IsSuccess);
await sutProvider.GetDependency<IUserService>().Received(1).CancelPremiumAsync(user);
await AssertSuccessfulUserOperations(sutProvider, [user], [orgUser]);
sutProvider.GetDependency<ILogger<DeleteClaimedOrganizationUserAccountCommand>>()
.Received(1)
.Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Is<object>(o => o.ToString()!.Contains($"Failed to cancel premium subscription for {user.Id}")),
gatewayException,
Arg.Any<Func<object, Exception?, string>>());
}
[Theory]
[BitAutoData]
public async Task CreateInternalRequests_CreatesCorrectRequestsForAllUsers(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user1,
User user2,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser1,
[OrganizationUser] OrganizationUser orgUser2)
{
orgUser1.UserId = user1.Id;
orgUser2.UserId = user2.Id;
var orgUserIds = new[] { orgUser1.Id, orgUser2.Id };
var orgUsers = new List<OrganizationUser> { orgUser1, orgUser2 };
var users = new[] { user1, user2 };
var claimedStatuses = new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, false } };
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(orgUsers);
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id)))
.Returns(users);
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(claimedStatuses);
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(callInfo =>
{
var requests = callInfo.Arg<IEnumerable<DeleteUserValidationRequest>>();
return requests.Select(r => CreateFailedValidationResult(r, new UserNotFoundError()));
});
// Act
await sutProvider.Sut.DeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId);
// Assert
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
.Received(1)
.ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>
requests.Count() == 2 &&
requests.Any(r => r.OrganizationUserId == orgUser1.Id &&
r.OrganizationId == organizationId &&
r.OrganizationUser == orgUser1 &&
r.User == user1 &&
r.DeletingUserId == deletingUserId &&
r.IsClaimed == true) &&
requests.Any(r => r.OrganizationUserId == orgUser2.Id &&
r.OrganizationId == organizationId &&
r.OrganizationUser == orgUser2 &&
r.User == user2 &&
r.DeletingUserId == deletingUserId &&
r.IsClaimed == false)));
}
[Theory]
[BitAutoData]
public async Task GetUsersAsync_WithNullUserIds_ReturnsEmptyCollection(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUserWithoutUserId)
{
orgUserWithoutUserId.UserId = null; // Intentionally setting to null for test case
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUserWithoutUserId });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()))
.Returns([]);
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(callInfo =>
{
var requests = callInfo.Arg<IEnumerable<DeleteUserValidationRequest>>();
return requests.Select(r => CreateFailedValidationResult(r, new UserNotFoundError()));
});
// Act
await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserWithoutUserId.Id], deletingUserId);
// Assert
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
.Received(1)
.ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>
requests.Count() == 1 &&
requests.Single().User == null));
await sutProvider.GetDependency<IUserRepository>().Received(1)
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()));
}
private static ValidationResult<DeleteUserValidationRequest> CreateSuccessfulValidationResult(
DeleteUserValidationRequest request) =>
ValidationResultHelpers.Valid(request);
private static ValidationResult<DeleteUserValidationRequest> CreateFailedValidationResult(
DeleteUserValidationRequest request,
Error error) =>
ValidationResultHelpers.Invalid(request, error);
private static void SetupRepositoryMocks(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
ICollection<OrganizationUser> orgUsers,
IEnumerable<User> users,
Guid organizationId,
Dictionary<Guid, bool> claimedStatuses)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(orgUsers);
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(users);
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(claimedStatuses);
}
private static void SetupValidatorMock(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
IEnumerable<ValidationResult<DeleteUserValidationRequest>> validationResults)
{
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
.Returns(validationResults);
}
private static async Task AssertSuccessfulUserOperations(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
IEnumerable<User> expectedUsers,
IEnumerable<OrganizationUser> expectedOrgUsers)
{
var userList = expectedUsers.ToList();
var orgUserList = expectedOrgUsers.ToList();
await sutProvider.GetDependency<IUserRepository>().Received(1)
.DeleteManyAsync(Arg.Is<IEnumerable<User>>(users =>
userList.All(expectedUser => users.Any(u => u.Id == expectedUser.Id))));
foreach (var user in userList)
{
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushLogOutAsync(user.Id);
}
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>
orgUserList.All(expectedOrgUser =>
events.Any(e => e.Item1.Id == expectedOrgUser.Id && e.Item2 == EventType.OrganizationUser_Deleted))));
}
private static async Task AssertNoUserOperations(SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider)
{
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().DeleteManyAsync(default);
await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushLogOutAsync(default);
await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventsAsync(default(IEnumerable<(OrganizationUser, EventType, DateTime?)>));
}
}

View File

@@ -0,0 +1,503 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
[SutProviderCustomize]
public class DeleteClaimedOrganizationUserAccountValidatorTests
{
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithValidSingleRequest_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
organizationUser.OrganizationId = organizationId;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsValid);
Assert.Equal(request, resultsList[0].Request);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithMultipleValidRequests_ReturnsAllValidResults(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user1,
User user2,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2)
{
orgUser1.UserId = user1.Id;
orgUser1.OrganizationId = organizationId;
orgUser2.UserId = user2.Id;
orgUser2.OrganizationId = organizationId;
var request1 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser1.Id,
OrganizationUser = orgUser1,
User = user1,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var request2 = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = orgUser2.Id,
OrganizationUser = orgUser2,
User = user2,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user1.Id);
SetupMocks(sutProvider, organizationId, user2.Id);
var results = await sutProvider.Sut.ValidateAsync([request1, request2]);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
Assert.All(resultsList, result => Assert.True(result.IsValid));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithNullUser_ReturnsUserNotFoundError(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = null,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<UserNotFoundError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId)
{
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = Guid.NewGuid(),
OrganizationUser = null,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<UserNotFoundError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithInvitedUser_ReturnsInvalidUserStatusError(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<InvalidUserStatusError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WhenDeletingYourself_ReturnsCannotDeleteYourselfError(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = user.Id,
IsClaimed = true
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<CannotDeleteYourselfError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithUnclaimedUser_ReturnsUserNotClaimedError(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = false
};
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<UserNotClaimedError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsNotOwner_ReturnsCannotDeleteOwnersError(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Admin);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<CannotDeleteOwnersError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsOwner_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsValid);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithSoleOwnerOfOrganization_ReturnsSoleOwnerError(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOnlyOwnerAsync(user.Id)
.Returns(1);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<SoleOwnerError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithSoleProviderOwner_ReturnsSoleProviderError(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id);
sutProvider.GetDependency<IProviderUserRepository>()
.GetCountByOnlyOwnerAsync(user.Id)
.Returns(1);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<SoleProviderError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_CustomUserDeletingAdmin_ReturnsCannotDeleteAdminsError(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Custom);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsError);
Assert.IsType<CannotDeleteAdminsError>(resultsList[0].AsError);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_AdminDeletingAdmin_ReturnsValidResult(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User user,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser)
{
organizationUser.UserId = user.Id;
var request = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = organizationUser.Id,
OrganizationUser = organizationUser,
User = user,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Admin);
var results = await sutProvider.Sut.ValidateAsync([request]);
var resultsList = results.ToList();
Assert.Single(resultsList);
Assert.True(resultsList[0].IsValid);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_WithMixedValidAndInvalidRequests_ReturnsCorrespondingResults(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
User validUser,
User invalidUser,
Guid organizationId,
Guid deletingUserId,
[OrganizationUser] OrganizationUser validOrgUser,
[OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser invalidOrgUser)
{
validOrgUser.UserId = validUser.Id;
invalidOrgUser.UserId = invalidUser.Id;
var validRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = validOrgUser.Id,
OrganizationUser = validOrgUser,
User = validUser,
DeletingUserId = deletingUserId,
IsClaimed = true
};
var invalidRequest = new DeleteUserValidationRequest
{
OrganizationId = organizationId,
OrganizationUserId = invalidOrgUser.Id,
OrganizationUser = invalidOrgUser,
User = invalidUser,
DeletingUserId = deletingUserId,
IsClaimed = true
};
SetupMocks(sutProvider, organizationId, validUser.Id);
var results = await sutProvider.Sut.ValidateAsync([validRequest, invalidRequest]);
var resultsList = results.ToList();
Assert.Equal(2, resultsList.Count);
var validResult = resultsList.First(r => r.Request == validRequest);
var invalidResult = resultsList.First(r => r.Request == invalidRequest);
Assert.True(validResult.IsValid);
Assert.True(invalidResult.IsError);
Assert.IsType<InvalidUserStatusError>(invalidResult.AsError);
}
private static void SetupMocks(
SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,
Guid organizationId,
Guid userId,
OrganizationUserType currentUserType = OrganizationUserType.Owner)
{
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(currentUserType == OrganizationUserType.Owner);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationAdmin(organizationId)
.Returns(currentUserType is OrganizationUserType.Owner or OrganizationUserType.Admin);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationCustom(organizationId)
.Returns(currentUserType is OrganizationUserType.Custom);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOnlyOwnerAsync(userId)
.Returns(0);
sutProvider.GetDependency<IProviderUserRepository>()
.GetCountByOnlyOwnerAsync(userId)
.Returns(0);
}
}

View File

@@ -1,526 +0,0 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
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 DeleteClaimedOrganizationUserAccountCommandTests
{
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithValidUser_DeletesUserAndLogsEvent(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user, Guid deletingUserId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser)
{
// Arrange
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IUserRepository>()
.GetByIdAsync(user.Id)
.Returns(user);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)),
includeProvider: Arg.Any<bool>())
.Returns(true);
// Act
await sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId);
// Assert
await sutProvider.GetDependency<IUserService>().Received(1).DeleteAsync(user);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Deleted);
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithUserNotFound_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
Guid organizationId, Guid organizationUserId)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId)
.Returns((OrganizationUser?)null);
// Act
var exception = await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationId, organizationUserId, null));
// Assert
Assert.Equal("Member not found.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_DeletingYourself_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser,
Guid deletingUserId)
{
// Arrange
organizationUser.UserId = user.Id = deletingUserId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
.Returns(user);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
// Assert
Assert.Equal("You cannot delete yourself.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WhenUserIsInvited_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser organizationUser)
{
// Arrange
organizationUser.UserId = null;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, null));
// Assert
Assert.Equal("You cannot delete a member with Invited status.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WhenCustomUserDeletesAdmin_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser,
Guid deletingUserId)
{
// Arrange
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
.Returns(user);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationCustom(organizationUser.OrganizationId)
.Returns(true);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
// Assert
Assert.Equal("Custom users can not delete admins.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_DeletingOwnerWhenNotOwner_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
Guid deletingUserId)
{
// Arrange
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
.Returns(user);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationUser.OrganizationId)
.Returns(false);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
// Assert
Assert.Equal("Only owners can delete other owners.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_DeletingLastConfirmedOwner_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
Guid deletingUserId)
{
// Arrange
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
.Returns(user);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)),
includeProvider: Arg.Any<bool>())
.Returns(false);
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId));
// Assert
Assert.Equal("Organization must have at least one confirmed owner.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteUserAsync_WithUserNotManaged_ThrowsException(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser)
{
// Arrange
organizationUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id)
.Returns(user);
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, false } });
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, null));
// Assert
Assert.Equal("Member is not claimed by the organization.", exception.Message);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user1, User user2, Guid organizationId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)
{
// Arrange
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
orgUser1.UserId = user1.Id;
orgUser2.UserId = user2.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser1, orgUser2 });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id)))
.Returns(new[] { user1, user2 });
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, true } });
// Act
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<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
&& events.Count(e => e.Item1.Id == orgUser2.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1));
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenUserNotFound_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
Guid organizationId,
Guid orgUserId)
{
// Act
var result = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUserId }, null);
// Assert
Assert.Single(result);
Assert.Equal(orgUserId, result.First().Item1);
Assert.Contains("Member not found.", result.First().Item2);
await sutProvider.GetDependency<IUserRepository>()
.DidNotReceiveWithAnyArgs()
.DeleteManyAsync(default);
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenDeletingYourself_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
User user, [OrganizationUser] OrganizationUser orgUser, Guid deletingUserId)
{
// Arrange
orgUser.UserId = user.Id = deletingUserId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
.Returns(new[] { user });
// Act
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
// Assert
Assert.Single(result);
Assert.Equal(orgUser.Id, result.First().Item1);
Assert.Contains("You cannot delete yourself.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenUserIsInvited_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser orgUser)
{
// Arrange
orgUser.UserId = null;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser });
// Act
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, null);
// Assert
Assert.Single(result);
Assert.Equal(orgUser.Id, result.First().Item1);
Assert.Contains("You cannot delete a member with Invited status.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenDeletingOwnerAsNonOwner_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
Guid deletingUserId)
{
// Arrange
orgUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(user.Id)))
.Returns(new[] { user });
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgUser.OrganizationId)
.Returns(false);
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
Assert.Single(result);
Assert.Equal(orgUser.Id, result.First().Item1);
Assert.Contains("Only owners can delete other owners.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenDeletingLastOwner_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
Guid deletingUserId)
{
// Arrange
orgUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(user.Id)))
.Returns(new[] { user });
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(orgUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any<IEnumerable<Guid>>(), Arg.Any<bool>())
.Returns(false);
// Act
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId);
// Assert
Assert.Single(result);
Assert.Equal(orgUser.Id, result.First().Item1);
Assert.Contains("Organization must have at least one confirmed owner.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_WhenUserNotManaged_ReturnsErrorMessage(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
{
// Arrange
orgUser.UserId = user.Id;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.UserId.Value)))
.Returns(new[] { user });
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, bool> { { orgUser.Id, false } });
// Act
var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, null);
// Assert
Assert.Single(result);
Assert.Equal(orgUser.Id, result.First().Item1);
Assert.Contains("Member is not claimed by the organization.", result.First().Item2);
await sutProvider.GetDependency<IUserService>().Received(0).DeleteAsync(Arg.Any<User>());
await sutProvider.GetDependency<IEventService>().Received(0)
.LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[BitAutoData]
public async Task DeleteManyUsersAsync_MixedValidAndInvalidUsers_ReturnsAppropriateResults(
SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider, User user1, User user3,
Guid organizationId,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser orgUser2,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser3)
{
// Arrange
orgUser1.UserId = user1.Id;
orgUser2.UserId = null;
orgUser3.UserId = user3.Id;
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<OrganizationUser> { orgUser1, orgUser2, orgUser3 });
sutProvider.GetDependency<IUserRepository>()
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user3.Id)))
.Returns(new[] { user1, user3 });
sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()
.GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser3.Id, false } });
// Act
var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUser1.Id, orgUser2.Id, orgUser3.Id }, null);
// Assert
Assert.Equal(3, results.Count());
Assert.Empty(results.First(r => r.Item1 == orgUser1.Id).Item2);
Assert.Equal("You cannot delete a member with Invited status.", results.First(r => r.Item1 == orgUser2.Id).Item2);
Assert.Equal("Member is not claimed by the organization.", results.First(r => r.Item1 == orgUser3.Id).Item2);
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

@@ -19,7 +19,6 @@ using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.StaticStore;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -30,7 +29,6 @@ using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
using static Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers.InviteUserOrganizationValidationRequestHelpers;
using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
@@ -54,7 +52,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites: [
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],
@@ -82,10 +80,6 @@ public class InviteOrganizationUserCommandTests
Assert.IsType<Failure<ScimInviteOrganizationUsersResponse>>(result);
Assert.Equal(NoUsersToInviteError.Code, (result as Failure<ScimInviteOrganizationUsersResponse>)!.Error.Message);
await sutProvider.GetDependency<IPaymentService>()
.DidNotReceiveWithAnyArgs()
.AdjustSeatsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
.DidNotReceiveWithAnyArgs()
.SendInvitesAsync(Arg.Any<SendInvitesRequest>());
@@ -112,7 +106,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites: [
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: orgUser.Email,
assignedCollections: [],
groups: [],
@@ -182,7 +176,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites: [
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],
@@ -257,7 +251,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites: [
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],
@@ -334,7 +328,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites:
[
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],
@@ -411,7 +405,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites: [
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],
@@ -459,10 +453,7 @@ public class InviteOrganizationUserCommandTests
// Assert
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
await sutProvider.GetDependency<IPaymentService>()
.AdjustSeatsAsync(organization, inviteOrganization.Plan, passwordManagerUpdate.UpdatedSeatTotal!.Value);
await orgRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(x => x.Seats == passwordManagerUpdate.UpdatedSeatTotal));
await orgRepository.Received(1).IncrementSeatCountAsync(organization.Id, passwordManagerUpdate.SeatsRequiredToAdd, request.PerformedAt.UtcDateTime);
await sutProvider.GetDependency<IApplicationCacheService>()
.Received(1)
@@ -492,7 +483,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites: [
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],
@@ -566,7 +557,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites: [
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],
@@ -633,11 +624,7 @@ public class InviteOrganizationUserCommandTests
.UpdateSubscriptionAsync(Arg.Any<SecretsManagerSubscriptionUpdate>());
// PM revert
await sutProvider.GetDependency<IPaymentService>()
.Received(2)
.AdjustSeatsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());
await orgRepository.Received(2).ReplaceAsync(Arg.Any<Organization>());
await orgRepository.Received(1).ReplaceAsync(Arg.Any<Organization>());
await sutProvider.GetDependency<IApplicationCacheService>().Received(2)
.UpsertOrganizationAbilityAsync(Arg.Any<Organization>());
@@ -669,7 +656,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites: [
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],
@@ -768,7 +755,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites: [
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],
@@ -863,7 +850,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites:
[
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],
@@ -942,7 +929,7 @@ public class InviteOrganizationUserCommandTests
var request = new InviteOrganizationUsersRequest(
invites:
[
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: user.Email,
assignedCollections: [],
groups: [],

View File

@@ -0,0 +1,137 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
[SutProviderCustomize]
public class ResendOrganizationInviteCommandTests
{
[Theory]
[BitAutoData]
public async Task ResendInviteAsync_WhenValidUserAndOrganization_SendsInvite(
Organization organization,
OrganizationUser organizationUser,
SutProvider<ResendOrganizationInviteCommand> sutProvider)
{
// Arrange
organizationUser.OrganizationId = organization.Id;
organizationUser.Status = OrganizationUserStatusType.Invited;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
// Act
await sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id);
// Assert
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
.Received(1)
.SendInvitesAsync(Arg.Is<SendInvitesRequest>(req =>
req.Organization == organization &&
req.Users.Length == 1 &&
req.Users[0] == organizationUser &&
req.InitOrganization == false));
}
[Theory]
[BitAutoData]
public async Task ResendInviteAsync_WhenInitOrganizationTrue_SendsInviteWithInitFlag(
Organization organization,
OrganizationUser organizationUser,
SutProvider<ResendOrganizationInviteCommand> sutProvider)
{
// Arrange
organizationUser.OrganizationId = organization.Id;
organizationUser.Status = OrganizationUserStatusType.Invited;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
// Act
await sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id, initOrganization: true);
// Assert
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
.Received(1)
.SendInvitesAsync(Arg.Is<SendInvitesRequest>(req =>
req.Organization == organization &&
req.Users.Length == 1 &&
req.Users[0] == organizationUser &&
req.InitOrganization == true));
}
[Theory]
[BitAutoData]
public async Task ResendInviteAsync_WhenOrganizationUserInvalid_ThrowsBadRequest(
Organization organization,
OrganizationUser organizationUser,
SutProvider<ResendOrganizationInviteCommand> sutProvider)
{
// Arrange
organizationUser.OrganizationId = organization.Id;
organizationUser.Status = OrganizationUserStatusType.Accepted;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
// Act + Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id));
Assert.Equal("User invalid.", ex.Message);
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
.DidNotReceive()
.SendInvitesAsync(Arg.Any<SendInvitesRequest>());
}
[Theory]
[BitAutoData]
public async Task ResendInviteAsync_WhenOrganizationNotFound_ThrowsBadRequest(
Organization organization,
OrganizationUser organizationUser,
SutProvider<ResendOrganizationInviteCommand> sutProvider)
{
// Arrange
organizationUser.OrganizationId = organization.Id;
organizationUser.Status = OrganizationUserStatusType.Invited;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns((Organization?)null);
// Act + Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id));
Assert.Equal("Organization invalid.", ex.Message);
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
.DidNotReceive()
.SendInvitesAsync(Arg.Any<SendInvitesRequest>());
}
}

View File

@@ -14,7 +14,6 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
@@ -36,13 +35,13 @@ public class InviteOrganizationUsersValidatorTests
{
Invites =
[
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: "test@email.com",
externalId: "test-external-id"),
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: "test2@email.com",
externalId: "test-external-id2"),
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: "test3@email.com",
externalId: "test-external-id3")
],
@@ -82,13 +81,13 @@ public class InviteOrganizationUsersValidatorTests
{
Invites =
[
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: "test@email.com",
externalId: "test-external-id"),
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: "test2@email.com",
externalId: "test-external-id2"),
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: "test3@email.com",
externalId: "test-external-id3")
],
@@ -126,13 +125,13 @@ public class InviteOrganizationUsersValidatorTests
{
Invites =
[
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: "test@email.com",
externalId: "test-external-id"),
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: "test2@email.com",
externalId: "test-external-id2"),
new OrganizationUserInvite(
new OrganizationUserInviteCommandModel(
email: "test3@email.com",
externalId: "test-external-id3")
],

View File

@@ -0,0 +1,83 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
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 RevokeOrganizationUserCommandTests
{
[Theory, BitAutoData]
public async Task RevokeUser_Success(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser] OrganizationUser organizationUser,
SutProvider<RevokeOrganizationUserCommand> sutProvider)
{
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
await sutProvider.Sut.RevokeUserAsync(organizationUser, owner.Id);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.RevokeAsync(organizationUser.Id);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
}
[Theory, BitAutoData]
public async Task RevokeUser_WithEventSystemUser_Success(
Organization organization,
[OrganizationUser] OrganizationUser organizationUser,
EventSystemUser eventSystemUser,
SutProvider<RevokeOrganizationUserCommand> sutProvider)
{
RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider);
await sutProvider.Sut.RevokeUserAsync(organizationUser, eventSystemUser);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.RevokeAsync(organizationUser.Id);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, eventSystemUser);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
}
private void RestoreRevokeUser_Setup(
Organization organization,
OrganizationUser? requestingOrganizationUser,
OrganizationUser targetOrganizationUser,
SutProvider<RevokeOrganizationUserCommand> sutProvider)
{
if (requestingOrganizationUser != null)
{
requestingOrganizationUser.OrganizationId = organization.Id;
}
targetOrganizationUser.OrganizationId = organization.Id;
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
}
}

View File

@@ -244,6 +244,44 @@ public class UpdateOrganizationUserCommandTests
Assert.Contains("User can only be an admin of one free organization.", exception.Message);
}
[Theory, BitAutoData]
public async Task UpdateUserAsync_WithMixedCollectionTypes_FiltersOutDefaultUserCollections(
OrganizationUser user, OrganizationUser originalUser, Collection sharedCollection, Collection defaultUserCollection,
Guid? savingUserId, SutProvider<UpdateOrganizationUserCommand> sutProvider, Organization organization)
{
user.Permissions = null;
sharedCollection.Type = CollectionType.SharedCollection;
defaultUserCollection.Type = CollectionType.DefaultUserCollection;
sharedCollection.OrganizationId = defaultUserCollection.OrganizationId = organization.Id;
Setup(sutProvider, organization, user, originalUser);
var collectionAccess = new List<CollectionAccessSelection>
{
new() { Id = sharedCollection.Id, ReadOnly = true, HidePasswords = false, Manage = false },
new() { Id = defaultUserCollection.Id, ReadOnly = false, HidePasswords = true, Manage = false }
};
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new List<Collection>
{
new() { Id = sharedCollection.Id, OrganizationId = user.OrganizationId, Type = CollectionType.SharedCollection },
new() { Id = defaultUserCollection.Id, OrganizationId = user.OrganizationId, Type = CollectionType.DefaultUserCollection }
});
await sutProvider.Sut.UpdateUserAsync(user, OrganizationUserType.User, savingUserId, collectionAccess, null);
// Verify that ReplaceAsync was called with only the shared collection (default user collection filtered out)
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(
user,
Arg.Is<IEnumerable<CollectionAccessSelection>>(collections =>
collections.Count() == 1 &&
collections.First().Id == sharedCollection.Id
)
);
}
private void Setup(SutProvider<UpdateOrganizationUserCommand> sutProvider, Organization organization,
OrganizationUser newUser, OrganizationUser oldUser)
{

View File

@@ -0,0 +1,54 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Pricing;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
[SutProviderCustomize]
public class GetOrganizationSubscriptionsToUpdateQueryTests
{
[Theory]
[BitAutoData]
public async Task GetOrganizationSubscriptionsToUpdateAsync_WhenNoOrganizationsNeedToBeSynced_ThenAnEmptyListIsReturned(
SutProvider<GetOrganizationSubscriptionsToUpdateQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>()
.GetOrganizationsForSubscriptionSyncAsync()
.Returns([]);
var result = await sutProvider.Sut.GetOrganizationSubscriptionsToUpdateAsync();
Assert.Empty(result);
}
[Theory]
[BitAutoData]
public async Task GetOrganizationSubscriptionsToUpdateAsync_WhenOrganizationsNeedToBeSynced_ThenUpdateIsReturnedWithCorrectPlanAndOrg(
Organization organization,
SutProvider<GetOrganizationSubscriptionsToUpdateQuery> sutProvider)
{
organization.PlanType = PlanType.EnterpriseAnnually2023;
sutProvider.GetDependency<IOrganizationRepository>()
.GetOrganizationsForSubscriptionSyncAsync()
.Returns([organization]);
sutProvider.GetDependency<IPricingClient>()
.ListPlans()
.Returns([new Enterprise2023Plan(true)]);
var result = await sutProvider.Sut.GetOrganizationSubscriptionsToUpdateAsync();
var matchingUpdate = result.FirstOrDefault(x => x.Organization.Id == organization.Id);
Assert.NotNull(matchingUpdate);
Assert.Equal(organization.PlanType, matchingUpdate.Plan!.Type);
Assert.Equal(organization, matchingUpdate.Organization);
}
}

View File

@@ -1,9 +1,9 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;

View File

@@ -0,0 +1,351 @@
using System.Security.Claims;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
[SutProviderCustomize]
public class SelfHostedOrganizationSignUpCommandTests
{
[Theory, BitAutoData]
public async Task SignUpAsync_WithValidRequest_CreatesOrganizationSuccessfully(
User owner, string ownerKey, string collectionName, string publicKey,
string privateKey, List<Device> devices,
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
var license = CreateValidOrganizationLicense(globalSettings);
SetupCommonMocks(sutProvider, owner);
SetupLicenseValidation(sutProvider, license);
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(owner.Id)
.Returns(devices);
// Act
var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey);
// Assert
Assert.NotNull(result.organization);
Assert.NotNull(result.organizationUser);
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1)
.CreateAsync(result.organization);
await sutProvider.GetDependency<IOrganizationApiKeyRepository>()
.Received(1)
.CreateAsync(Arg.Is<OrganizationApiKey>(key =>
key.OrganizationId == result.organization.Id &&
key.Type == OrganizationApiKeyType.Default &&
!string.IsNullOrEmpty(key.ApiKey)));
await sutProvider.GetDependency<IApplicationCacheService>()
.Received(1)
.UpsertOrganizationAbilityAsync(result.organization);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.CreateAsync(Arg.Is<OrganizationUser>(user =>
user.OrganizationId == result.organization.Id &&
user.UserId == owner.Id &&
user.Key == ownerKey &&
user.Type == OrganizationUserType.Owner &&
user.Status == OrganizationUserStatusType.Confirmed));
await sutProvider.GetDependency<ICollectionRepository>()
.Received(1)
.CreateAsync(
Arg.Is<Collection>(c => c.Name == collectionName && c.OrganizationId == result.organization.Id),
Arg.Is<IEnumerable<CollectionAccessSelection>>(groups => groups == null),
Arg.Is<IEnumerable<CollectionAccessSelection>>(access =>
access.Any(a => a.Id == result.organizationUser.Id && a.Manage && !a.ReadOnly && !a.HidePasswords)));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushSyncOrgKeysAsync(owner.Id);
}
[Theory, BitAutoData]
public async Task SignUpAsync_WithPremiumLicense_ThrowsBadRequestException(
User owner, string ownerKey, string collectionName,
string publicKey, string privateKey,
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
var license = CreateValidOrganizationLicense(globalSettings, LicenseType.User);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));
Assert.Contains("Premium licenses cannot be applied to an organization", exception.Message);
}
[Theory, BitAutoData]
public async Task SignUpAsync_WithInvalidLicense_ThrowsBadRequestException(
User owner, string ownerKey, string collectionName,
string publicKey, string privateKey,
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
var license = CreateValidOrganizationLicense(globalSettings);
license.CanUse(globalSettings, sutProvider.GetDependency<ILicensingService>(), null, out _)
.Returns(false);
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));
}
[Theory, BitAutoData]
public async Task SignUpAsync_WithLicenseAlreadyInUse_ThrowsBadRequestException(
User owner, string ownerKey, string collectionName,
string publicKey, string privateKey, Organization existingOrganization,
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
var license = CreateValidOrganizationLicense(globalSettings);
existingOrganization.LicenseKey = license.LicenseKey;
SetupCommonMocks(sutProvider, owner);
SetupLicenseValidation(sutProvider, license);
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyByEnabledAsync()
.Returns(new List<Organization> { existingOrganization });
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));
Assert.Contains("License is already in use", exception.Message);
}
[Theory, BitAutoData]
public async Task SignUpAsync_WithSingleOrgPolicy_ThrowsBadRequestException(
User owner, string ownerKey, string collectionName,
string publicKey, string privateKey,
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
var license = CreateValidOrganizationLicense(globalSettings);
SetupCommonMocks(sutProvider, owner);
SetupLicenseValidation(sutProvider, license);
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(owner.Id, PolicyType.SingleOrg)
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));
Assert.Contains("You may not create an organization", exception.Message);
}
[Theory, BitAutoData]
public async Task SignUpAsync_WithClaimsPrincipal_UsesClaimsPrincipalToCreateOrganization(
User owner, string ownerKey, string collectionName,
string publicKey, string privateKey, ClaimsPrincipal claimsPrincipal,
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
var license = CreateValidOrganizationLicense(globalSettings);
SetupCommonMocks(sutProvider, owner);
SetupLicenseValidation(sutProvider, license);
sutProvider.GetDependency<ILicensingService>()
.GetClaimsPrincipalFromLicense(license)
.Returns(claimsPrincipal);
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(owner.Id)
.Returns(new List<Device>());
// Act
var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey);
// Assert
Assert.NotNull(result.organization);
Assert.NotNull(result.organizationUser);
sutProvider.GetDependency<ILicensingService>()
.Received(1)
.GetClaimsPrincipalFromLicense(license);
}
[Theory, BitAutoData]
public async Task SignUpAsync_WithoutCollectionName_DoesNotCreateCollection(
User owner, string ownerKey, string publicKey, string privateKey,
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
var license = CreateValidOrganizationLicense(globalSettings);
SetupCommonMocks(sutProvider, owner);
SetupLicenseValidation(sutProvider, license);
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(owner.Id)
.Returns(new List<Device>());
// Act
var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, null, publicKey, privateKey);
// Assert
Assert.NotNull(result.organization);
Assert.NotNull(result.organizationUser);
await sutProvider.GetDependency<ICollectionRepository>()
.DidNotReceive()
.CreateAsync(Arg.Any<Collection>(), Arg.Is<IEnumerable<CollectionAccessSelection>>(x => true), Arg.Is<IEnumerable<CollectionAccessSelection>>(x => true));
}
[Theory, BitAutoData]
public async Task SignUpAsync_WithDevices_RegistersDevicesForPushNotifications(
User owner, string ownerKey, string collectionName,
string publicKey, string privateKey, List<Device> devices,
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
var license = CreateValidOrganizationLicense(globalSettings);
foreach (var device in devices)
{
device.PushToken = "push-token-" + device.Id;
}
SetupCommonMocks(sutProvider, owner);
SetupLicenseValidation(sutProvider, license);
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(owner.Id)
.Returns(devices);
// Act
var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey);
// Assert
Assert.NotNull(result.organization);
Assert.NotNull(result.organizationUser);
var expectedDeviceIds = devices.Select(d => d.Id.ToString());
await sutProvider.GetDependency<IPushRegistrationService>()
.Received(1)
.AddUserRegistrationOrganizationAsync(
Arg.Is<IEnumerable<string>>(ids => ids.SequenceEqual(expectedDeviceIds)),
result.organization.Id.ToString());
}
[Theory, BitAutoData]
public async Task SignUpAsync_OnException_CleansUpOrganization(
User owner, string ownerKey, string collectionName,
string publicKey, string privateKey,
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
var license = CreateValidOrganizationLicense(globalSettings);
SetupCommonMocks(sutProvider, owner);
SetupLicenseValidation(sutProvider, license);
sutProvider.GetDependency<IOrganizationApiKeyRepository>()
.CreateAsync(Arg.Any<OrganizationApiKey>())
.Throws(new Exception("Test exception"));
// Act & Assert
await Assert.ThrowsAsync<Exception>(
() => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1)
.DeleteAsync(Arg.Any<Organization>());
await sutProvider.GetDependency<IApplicationCacheService>()
.Received(1)
.DeleteOrganizationAbilityAsync(Arg.Any<Guid>());
}
private void SetupCommonMocks(
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider,
User owner)
{
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
sutProvider.GetDependency<IOrganizationRepository>()
.CreateAsync(Arg.Any<Organization>())
.Returns(callInfo =>
{
var org = callInfo.Arg<Organization>();
org.Id = Guid.NewGuid();
return Task.FromResult(org);
});
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(owner.Id, PolicyType.SingleOrg)
.Returns(false);
globalSettings.LicenseDirectory.Returns("/tmp/licenses");
}
private void SetupLicenseValidation(
SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider,
OrganizationLicense license)
{
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
sutProvider.GetDependency<ILicensingService>()
.VerifyLicense(license)
.Returns(true);
license.CanUse(globalSettings, sutProvider.GetDependency<ILicensingService>(), null, out _)
.Returns(true);
}
private OrganizationLicense CreateValidOrganizationLicense(
IGlobalSettings globalSettings,
LicenseType licenseType = LicenseType.Organization)
{
return new OrganizationLicense
{
LicenseType = licenseType,
Signature = Guid.NewGuid().ToString().Replace('-', '+'),
Issued = DateTime.UtcNow.AddDays(-1),
Expires = DateTime.UtcNow.AddDays(10),
Version = OrganizationLicense.CurrentLicenseFileVersion,
InstallationId = globalSettings.Installation.Id,
Enabled = true,
SelfHost = true
};
}
}

View File

@@ -0,0 +1,146 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Models.StaticStore;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
[SutProviderCustomize]
public class UpdateOrganizationSubscriptionCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateOrganizationSubscriptionAsync_WhenNoSubscriptionsNeedToBeUpdated_ThenNoSyncsOccur(
SutProvider<UpdateOrganizationSubscriptionCommand> sutProvider)
{
// Arrange
OrganizationSubscriptionUpdate[] subscriptionsToUpdate = [];
// Act
await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate);
await sutProvider.GetDependency<IPaymentService>()
.DidNotReceive()
.AdjustSeatsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());
await sutProvider.GetDependency<IOrganizationRepository>()
.DidNotReceive()
.UpdateSuccessfulOrganizationSyncStatusAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<DateTime>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationSubscriptionAsync_WhenOrgUpdatePassedIn_ThenSyncedThroughPaymentService(
Organization organization,
SutProvider<UpdateOrganizationSubscriptionCommand> sutProvider)
{
// Arrange
organization.PlanType = PlanType.EnterpriseAnnually2023;
organization.Seats = 2;
OrganizationSubscriptionUpdate[] subscriptionsToUpdate =
[new() { Organization = organization, Plan = new Enterprise2023Plan(true) }];
// Act
await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate);
await sutProvider.GetDependency<IPaymentService>()
.Received(1)
.AdjustSeatsAsync(
Arg.Is<Organization>(x => x.Id == organization.Id),
Arg.Is<Plan>(x => x.Type == organization.PlanType),
organization.Seats!.Value);
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1)
.UpdateSuccessfulOrganizationSyncStatusAsync(
Arg.Is<IEnumerable<Guid>>(x => x.Contains(organization.Id)),
Arg.Any<DateTime>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationSubscriptionAsync_WhenOrgUpdateFails_ThenSyncDoesNotOccur(
Organization organization,
Exception exception,
SutProvider<UpdateOrganizationSubscriptionCommand> sutProvider)
{
// Arrange
organization.PlanType = PlanType.EnterpriseAnnually2023;
organization.Seats = 2;
OrganizationSubscriptionUpdate[] subscriptionsToUpdate =
[new() { Organization = organization, Plan = new Enterprise2023Plan(true) }];
sutProvider.GetDependency<IPaymentService>()
.AdjustSeatsAsync(
Arg.Is<Organization>(x => x.Id == organization.Id),
Arg.Is<Plan>(x => x.Type == organization.PlanType),
organization.Seats!.Value).ThrowsAsync(exception);
// Act
await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate);
await sutProvider.GetDependency<IOrganizationRepository>()
.DidNotReceive()
.UpdateSuccessfulOrganizationSyncStatusAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<DateTime>());
}
[Theory]
[BitAutoData]
public async Task UpdateOrganizationSubscriptionAsync_WhenOneOrgUpdateFailsAndAnotherSucceeds_ThenSyncOccursForTheSuccessfulOrg(
Organization successfulOrganization,
Organization failedOrganization,
Exception exception,
SutProvider<UpdateOrganizationSubscriptionCommand> sutProvider)
{
// Arrange
successfulOrganization.PlanType = PlanType.EnterpriseAnnually2023;
successfulOrganization.Seats = 2;
failedOrganization.PlanType = PlanType.EnterpriseAnnually2023;
failedOrganization.Seats = 2;
OrganizationSubscriptionUpdate[] subscriptionsToUpdate =
[
new() { Organization = successfulOrganization, Plan = new Enterprise2023Plan(true) },
new() { Organization = failedOrganization, Plan = new Enterprise2023Plan(true) }
];
sutProvider.GetDependency<IPaymentService>()
.AdjustSeatsAsync(
Arg.Is<Organization>(x => x.Id == failedOrganization.Id),
Arg.Is<Plan>(x => x.Type == failedOrganization.PlanType),
failedOrganization.Seats!.Value).ThrowsAsync(exception);
// Act
await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate);
await sutProvider.GetDependency<IPaymentService>()
.Received(1)
.AdjustSeatsAsync(
Arg.Is<Organization>(x => x.Id == successfulOrganization.Id),
Arg.Is<Plan>(x => x.Type == successfulOrganization.PlanType),
successfulOrganization.Seats!.Value);
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1)
.UpdateSuccessfulOrganizationSyncStatusAsync(
Arg.Is<IEnumerable<Guid>>(x => x.Contains(successfulOrganization.Id)),
Arg.Any<DateTime>());
await sutProvider.GetDependency<IOrganizationRepository>()
.DidNotReceive()
.UpdateSuccessfulOrganizationSyncStatusAsync(
Arg.Is<IEnumerable<Guid>>(x => x.Contains(failedOrganization.Id)),
Arg.Any<DateTime>());
}
}

View File

@@ -79,4 +79,73 @@ public class PolicyRequirementQueryTests
Assert.Empty(requirement.Policies);
}
[Theory, BitAutoData]
public async Task GetManyByOrganizationIdAsync_IgnoresOtherPolicyTypes(Guid organizationId)
{
var policyRepository = Substitute.For<IPolicyRepository>();
var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, OrganizationUserId = Guid.NewGuid() };
var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.RequireSso, OrganizationUserId = Guid.NewGuid() };
// Force the repository to return both policies even though that is not the expected result
policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg)
.Returns([thisPolicy, otherPolicy]);
var factory = new TestPolicyRequirementFactory(_ => true);
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
var organizationUserIds = await sut.GetManyByOrganizationIdAsync<TestPolicyRequirement>(organizationId);
await policyRepository.Received(1).GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg);
Assert.Contains(thisPolicy.OrganizationUserId, organizationUserIds);
Assert.DoesNotContain(otherPolicy.OrganizationUserId, organizationUserIds);
}
[Theory, BitAutoData]
public async Task GetManyByOrganizationIdAsync_CallsEnforceCallback(Guid organizationId)
{
var policyRepository = Substitute.For<IPolicyRepository>();
var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, OrganizationUserId = Guid.NewGuid() };
var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, OrganizationUserId = Guid.NewGuid() };
policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg).Returns([thisPolicy, otherPolicy]);
var callback = Substitute.For<Func<PolicyDetails, bool>>();
callback(Arg.Any<PolicyDetails>()).Returns(x => x.Arg<PolicyDetails>() == thisPolicy);
var factory = new TestPolicyRequirementFactory(callback);
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
var organizationUserIds = await sut.GetManyByOrganizationIdAsync<TestPolicyRequirement>(organizationId);
Assert.Contains(thisPolicy.OrganizationUserId, organizationUserIds);
Assert.DoesNotContain(otherPolicy.OrganizationUserId, organizationUserIds);
callback.Received()(Arg.Is<PolicyDetails>(p => p == thisPolicy));
callback.Received()(Arg.Is<PolicyDetails>(p => p == otherPolicy));
}
[Theory, BitAutoData]
public async Task GetManyByOrganizationIdAsync_ThrowsIfNoFactoryRegistered(Guid organizationId)
{
var policyRepository = Substitute.For<IPolicyRepository>();
var sut = new PolicyRequirementQuery(policyRepository, []);
var exception = await Assert.ThrowsAsync<NotImplementedException>(()
=> sut.GetManyByOrganizationIdAsync<TestPolicyRequirement>(organizationId));
Assert.Contains("No Requirement Factory found", exception.Message);
}
[Theory, BitAutoData]
public async Task GetManyByOrganizationIdAsync_HandlesNoPolicies(Guid organizationId)
{
var policyRepository = Substitute.For<IPolicyRepository>();
policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg).Returns([]);
var factory = new TestPolicyRequirementFactory(x => x.IsProvider);
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
var organizationUserIds = await sut.GetManyByOrganizationIdAsync<TestPolicyRequirement>(organizationId);
Assert.Empty(organizationUserIds);
}
}

View File

@@ -0,0 +1,75 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
[SutProviderCustomize]
public class MasterPasswordPolicyRequirementFactoryTests
{
[Theory, BitAutoData]
public void MasterPasswordPolicyData_CombineWith_Joins_Policy_Options(SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
{
var mpd1 = JsonSerializer.Serialize(new MasterPasswordPolicyData { MinLength = 20, RequireLower = false, RequireSpecial = false });
var mpd2 = JsonSerializer.Serialize(new MasterPasswordPolicyData { RequireLower = true });
var mpd3 = JsonSerializer.Serialize(new MasterPasswordPolicyData { RequireSpecial = true });
var policyDetails1 = new PolicyDetails
{
PolicyType = PolicyType.MasterPassword,
PolicyData = mpd1
};
var policyDetails2 = new PolicyDetails
{
PolicyType = PolicyType.MasterPassword,
PolicyData = mpd2
};
var policyDetails3 = new PolicyDetails
{
PolicyType = PolicyType.MasterPassword,
PolicyData = mpd3
};
var actual = sutProvider.Sut.Create([policyDetails1, policyDetails2, policyDetails3]);
Assert.NotNull(actual);
Assert.True(actual.Enabled);
Assert.True(actual.EnforcedOptions.RequireLower);
Assert.True(actual.EnforcedOptions.RequireSpecial);
Assert.Equal(20, actual.EnforcedOptions.MinLength);
}
[Theory, BitAutoData]
public void MasterPassword_IsFalse_IfNoPolicies(SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create([]);
Assert.False(actual.Enabled);
Assert.Null(actual.EnforcedOptions);
}
[Theory, BitAutoData]
public void MasterPassword_IsTrue_IfAnyDisableSendPolicies(
[PolicyDetails(PolicyType.MasterPassword)] PolicyDetails[] policies,
SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create(policies);
Assert.True(actual.Enabled);
Assert.NotNull(actual.EnforcedOptions);
Assert.NotNull(actual.EnforcedOptions.EnforceOnLogin);
Assert.NotNull(actual.EnforcedOptions.RequireLower);
Assert.NotNull(actual.EnforcedOptions.RequireNumbers);
Assert.NotNull(actual.EnforcedOptions.RequireSpecial);
Assert.NotNull(actual.EnforcedOptions.RequireUpper);
Assert.Null(actual.EnforcedOptions.MinComplexity);
Assert.Null(actual.EnforcedOptions.MinLength);
}
}

View File

@@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Enums;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -30,24 +31,85 @@ public class OrganizationDataOwnershipPolicyRequirementFactoryTests
}
[Theory, BitAutoData]
public void RequiresDefaultCollection_WithNoPolicies_ReturnsFalse(
Guid organizationId,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
public void PolicyType_ReturnsOrganizationDataOwnership(SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create([]);
Assert.False(actual.RequiresDefaultCollection(organizationId));
Assert.Equal(PolicyType.OrganizationDataOwnership, sutProvider.Sut.PolicyType);
}
[Theory, BitAutoData]
public void RequiresDefaultCollection_WithOrganizationDataOwnershipPolicies_ReturnsCorrectResult(
[PolicyDetails(PolicyType.OrganizationDataOwnership)] PolicyDetails[] policies,
Guid nonPolicyOrganizationId,
public void GetDefaultCollectionRequestOnPolicyEnable_WithConfirmedUser_ReturnsTrue(
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
// Arrange
var requirement = sutProvider.Sut.Create(policies);
var expectedOrganizationUserId = policies[0].OrganizationUserId;
var organizationId = policies[0].OrganizationId;
// Act
var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId);
// Assert
Assert.Equal(expectedOrganizationUserId, result.OrganizationUserId);
Assert.True(result.ShouldCreateDefaultCollection);
}
[Theory, BitAutoData]
public void GetDefaultCollectionRequestOnPolicyEnable_WithAcceptedUser_ReturnsFalse(
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Accepted)] PolicyDetails[] policies,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create(policies);
// Arrange
var requirement = sutProvider.Sut.Create(policies);
var organizationId = policies[0].OrganizationId;
Assert.True(actual.RequiresDefaultCollection(policies[0].OrganizationId));
Assert.False(actual.RequiresDefaultCollection(nonPolicyOrganizationId));
// Act
var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId);
// Assert
Assert.Equal(Guid.Empty, result.OrganizationUserId);
Assert.False(result.ShouldCreateDefaultCollection);
}
[Theory, BitAutoData]
public void GetDefaultCollectionRequestOnPolicyEnable_WithNoPolicies_ReturnsFalse(
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
// Arrange
var requirement = sutProvider.Sut.Create([]);
var organizationId = Guid.NewGuid();
// Act
var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId);
// Assert
Assert.Equal(Guid.Empty, result.OrganizationUserId);
Assert.False(result.ShouldCreateDefaultCollection);
}
[Theory, BitAutoData]
public void GetDefaultCollectionRequestOnPolicyEnable_WithMixedStatuses(
[PolicyDetails(PolicyType.OrganizationDataOwnership)] PolicyDetails[] policies,
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
{
// Arrange
var requirement = sutProvider.Sut.Create(policies);
var confirmedPolicy = policies[0];
var acceptedPolicy = policies[1];
confirmedPolicy.OrganizationUserStatus = OrganizationUserStatusType.Confirmed;
acceptedPolicy.OrganizationUserStatus = OrganizationUserStatusType.Accepted;
// Act
var confirmedResult = requirement.GetDefaultCollectionRequestOnPolicyEnable(confirmedPolicy.OrganizationId);
var acceptedResult = requirement.GetDefaultCollectionRequestOnPolicyEnable(acceptedPolicy.OrganizationId);
// Assert
Assert.Equal(Guid.Empty, acceptedResult.OrganizationUserId);
Assert.False(acceptedResult.ShouldCreateDefaultCollection);
Assert.Equal(confirmedPolicy.OrganizationUserId, confirmedResult.OrganizationUserId);
Assert.True(confirmedResult.ShouldCreateDefaultCollection);
}
}

Some files were not shown because too many files have changed in this diff Show More