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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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>>());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
370
test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs
Normal file
370
test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
190
test/Api.Test/Billing/Attributes/InjectProviderAttributeTests.cs
Normal file
190
test/Api.Test/Billing/Attributes/InjectProviderAttributeTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
129
test/Api.Test/Billing/Attributes/InjectUserAttributesTests.cs
Normal file
129
test/Api.Test/Billing/Attributes/InjectUserAttributesTests.cs
Normal 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"]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
1165
test/Api.Test/Dirt/OrganizationReportsControllerTests.cs
Normal file
1165
test/Api.Test/Dirt/OrganizationReportsControllerTests.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
121
test/Api.Test/Public/Controllers/CollectionsControllerTests.cs
Normal file
121
test/Api.Test/Public/Controllers/CollectionsControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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;
|
||||
|
||||
36
test/Api.Test/Utilities/KdfSettingsValidatorTests.cs
Normal file
36
test/Api.Test/Utilities/KdfSettingsValidatorTests.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
242
test/Billing.Test/Services/SetupIntentSucceededHandlerTests.cs
Normal file
242
test/Billing.Test/Services/SetupIntentSucceededHandlerTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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" } }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 } }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -33,3 +33,5 @@ public class PolicyDetailsAttribute(
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
=> new PolicyDetailsCustomization(policyType, userType, isProvider, userStatus);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?)>));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user