mirror of
https://github.com/bitwarden/server
synced 2026-01-19 00:43:47 +00:00
Merge branch 'main' into tools/pm-21918/send-authentication-commands
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
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.DeleteClaimedAccountvNext;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
@@ -30,6 +33,10 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor)
|
||||
.Returns(true);
|
||||
});
|
||||
_client = _factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
@@ -42,6 +49,91 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
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)]
|
||||
@@ -57,11 +149,36 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
Ids = new List<Guid> { Guid.NewGuid() }
|
||||
};
|
||||
|
||||
var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/remove", request);
|
||||
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)]
|
||||
@@ -74,7 +191,7 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
|
||||
var userToRemove = Guid.NewGuid();
|
||||
|
||||
var httpResponse = await _client.DeleteAsync($"organizations/{_organization.Id}/users/{userToRemove}");
|
||||
var httpResponse = await _client.DeleteAsync($"organizations/{_organization.Id}/users/{userToRemove}/delete-account");
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,14 +2,11 @@
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.AdminConsole.Import;
|
||||
@@ -25,12 +22,6 @@ public class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture<ApiApp
|
||||
public ImportOrganizationUsersAndGroupsCommandTests(ApiApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_factory.SubstituteService((IFeatureService featureService)
|
||||
=>
|
||||
{
|
||||
featureService.IsEnabled(FeatureFlagKeys.DirectoryConnectorPreventUserRemoval)
|
||||
.Returns(true);
|
||||
});
|
||||
_client = _factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -271,7 +271,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)));
|
||||
}
|
||||
@@ -330,27 +330,6 @@ public class OrganizationUsersControllerTests
|
||||
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_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,32 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
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: "")]
|
||||
|
||||
@@ -84,7 +84,7 @@ public class OrganizationIntegrationRequestModelTests
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
|
||||
Assert.Contains("must include valid configuration", results[0].ErrorMessage);
|
||||
Assert.Contains("Must include valid", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -114,7 +114,7 @@ public class OrganizationIntegrationRequestModelTests
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
|
||||
Assert.Contains("must include valid configuration", results[0].ErrorMessage);
|
||||
Assert.Contains("Must include valid", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -130,7 +130,7 @@ public class OrganizationIntegrationRequestModelTests
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
|
||||
Assert.Contains("must include valid configuration", results[0].ErrorMessage);
|
||||
Assert.Contains("Must include valid", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -147,6 +147,54 @@ public class OrganizationIntegrationRequestModelTests
|
||||
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]
|
||||
public void Validate_UnknownIntegrationType_ReturnsUnrecognizedError()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class AuthRequestsControllerTests
|
||||
.Returns([authRequest]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Get();
|
||||
var result = await sutProvider.Sut.GetAll();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +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.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
|
||||
@@ -177,7 +177,7 @@ public class CollectionsControllerTests
|
||||
.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).GetManySharedCollectionsByOrganizationIdAsync(organization.Id);
|
||||
|
||||
@@ -219,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);
|
||||
|
||||
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
@@ -1,14 +1,12 @@
|
||||
using AutoFixture;
|
||||
using Bit.Api.Dirt.Controllers;
|
||||
using Bit.Api.Dirt.Models;
|
||||
using Bit.Api.Dirt.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@@ -144,323 +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);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void CreateOrganizationReportSummary_ReturnsNoContent_WhenAccessGranted(SutProvider<ReportsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
var model = new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
EncryptedData = "mock-data",
|
||||
EncryptionKey = "mock-key",
|
||||
Date = DateTime.UtcNow
|
||||
};
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(orgId).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.CreateOrganizationReportSummary(model);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void CreateOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied(SutProvider<ReportsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
var model = new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
EncryptedData = "mock-data",
|
||||
EncryptionKey = "mock-key",
|
||||
Date = DateTime.UtcNow
|
||||
};
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(orgId).Returns(false);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<Bit.Core.Exceptions.NotFoundException>(
|
||||
() => sutProvider.Sut.CreateOrganizationReportSummary(model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void GetOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied(
|
||||
SutProvider<ReportsController> sutProvider
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(orgId).Returns(false);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<Bit.Core.Exceptions.NotFoundException>(
|
||||
() => sutProvider.Sut.GetOrganizationReportSummary(orgId, DateOnly.FromDateTime(DateTime.UtcNow), DateOnly.FromDateTime(DateTime.UtcNow)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void GetOrganizationReportSummary_returnsExpectedResult(
|
||||
SutProvider<ReportsController> sutProvider
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
var dates = new[]
|
||||
{
|
||||
DateOnly.FromDateTime(DateTime.UtcNow),
|
||||
DateOnly.FromDateTime(DateTime.UtcNow.AddMonths(-1))
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(orgId).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.GetOrganizationReportSummary(orgId, dates[0], dates[1]);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void CreateOrganizationReportSummary_ReturnsNoContent_WhenModelIsValidAndAccessGranted(
|
||||
SutProvider<ReportsController> sutProvider
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
var model = new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
EncryptedData = "mock-data",
|
||||
EncryptionKey = "mock-key"
|
||||
};
|
||||
sutProvider.Sut.ModelState.Clear();
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(orgId).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.CreateOrganizationReportSummary(model);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void CreateOrganizationReportSummary_ThrowsBadRequestException_WhenModelStateIsInvalid(
|
||||
SutProvider<ReportsController> sutProvider
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
var model = new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
EncryptedData = "mock-data",
|
||||
EncryptionKey = "mock-key"
|
||||
};
|
||||
sutProvider.Sut.ModelState.AddModelError("key", "error");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<BadRequestException>(() => sutProvider.Sut.CreateOrganizationReportSummary(model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void UpdateOrganizationReportSummary_ReturnsNoContent_WhenModelIsValidAndAccessGranted(
|
||||
SutProvider<ReportsController> sutProvider
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
var model = new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
EncryptedData = "mock-data",
|
||||
EncryptionKey = "mock-key"
|
||||
};
|
||||
sutProvider.Sut.ModelState.Clear();
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(orgId).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.UpdateOrganizationReportSummary(model);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void UpdateOrganizationReportSummary_ThrowsBadRequestException_WhenModelStateIsInvalid(
|
||||
SutProvider<ReportsController> sutProvider
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
var model = new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
EncryptedData = "mock-data",
|
||||
EncryptionKey = "mock-key"
|
||||
};
|
||||
sutProvider.Sut.ModelState.AddModelError("key", "error");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<BadRequestException>(() => sutProvider.Sut.UpdateOrganizationReportSummary(model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void UpdateOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied(
|
||||
SutProvider<ReportsController> sutProvider
|
||||
)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
var model = new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
EncryptedData = "mock-data",
|
||||
EncryptionKey = "mock-key"
|
||||
};
|
||||
sutProvider.Sut.ModelState.Clear();
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(orgId).Returns(false);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotFoundException>(() => sutProvider.Sut.UpdateOrganizationReportSummary(model));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -126,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);
|
||||
@@ -186,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>()
|
||||
@@ -257,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>()
|
||||
@@ -324,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);
|
||||
@@ -387,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);
|
||||
@@ -457,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>()
|
||||
@@ -535,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
|
||||
@@ -610,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>()
|
||||
@@ -685,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>()
|
||||
@@ -753,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>()
|
||||
|
||||
@@ -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;
|
||||
@@ -126,7 +127,7 @@ public class FreshdeskControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_invalid_onyx_response_results_in_BadRequest(
|
||||
public async Task PostWebhookOnyxAi_invalid_onyx_response_results_is_logged(
|
||||
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
@@ -150,8 +151,18 @@ 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]
|
||||
@@ -174,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))
|
||||
@@ -195,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" } }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ 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;
|
||||
@@ -33,7 +32,6 @@ 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 IOrganizationEnableCommand _organizationEnableCommand;
|
||||
private readonly IOrganizationDisableCommand _organizationDisableCommand;
|
||||
@@ -42,6 +40,7 @@ public class SubscriptionUpdatedHandlerTests
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly IScheduler _scheduler;
|
||||
private readonly IPushNotificationAdapter _pushNotificationAdapter;
|
||||
private readonly SubscriptionUpdatedHandler _sut;
|
||||
|
||||
public SubscriptionUpdatedHandlerTests()
|
||||
@@ -53,7 +52,6 @@ public class SubscriptionUpdatedHandlerTests
|
||||
_organizationSponsorshipRenewCommand = Substitute.For<IOrganizationSponsorshipRenewCommand>();
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_providerService = Substitute.For<IProviderService>();
|
||||
_pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
var schedulerFactory = Substitute.For<ISchedulerFactory>();
|
||||
_organizationEnableCommand = Substitute.For<IOrganizationEnableCommand>();
|
||||
@@ -64,6 +62,7 @@ public class SubscriptionUpdatedHandlerTests
|
||||
_providerService = Substitute.For<IProviderService>();
|
||||
var logger = Substitute.For<ILogger<SubscriptionUpdatedHandler>>();
|
||||
_scheduler = Substitute.For<IScheduler>();
|
||||
_pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();
|
||||
|
||||
schedulerFactory.GetScheduler().Returns(_scheduler);
|
||||
|
||||
@@ -74,7 +73,6 @@ public class SubscriptionUpdatedHandlerTests
|
||||
_stripeFacade,
|
||||
_organizationSponsorshipRenewCommand,
|
||||
_userService,
|
||||
_pushNotificationService,
|
||||
_organizationRepository,
|
||||
schedulerFactory,
|
||||
_organizationEnableCommand,
|
||||
@@ -83,7 +81,8 @@ public class SubscriptionUpdatedHandlerTests
|
||||
_featureService,
|
||||
_providerRepository,
|
||||
_providerService,
|
||||
logger);
|
||||
logger,
|
||||
_pushNotificationAdapter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -540,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]
|
||||
|
||||
@@ -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));
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -98,8 +98,6 @@ public class ImportOrganizationUsersAndGroupsCommandTests
|
||||
SetupOrganizationConfigForImport(sutProvider, org, existingUsers, []);
|
||||
|
||||
// Existing user does not have a master password
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.DirectoryConnectorPreventUserRemoval)
|
||||
.Returns(true);
|
||||
existingUsers.First().HasMasterPassword = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
|
||||
@@ -479,7 +479,7 @@ public class ConfirmOrganizationUserCommandTests
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.CreateDefaultCollectionsAsync(
|
||||
.UpsertDefaultCollectionsAsync(
|
||||
organization.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.Id)),
|
||||
collectionName);
|
||||
@@ -505,7 +505,7 @@ public class ConfirmOrganizationUserCommandTests
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -531,6 +531,6 @@ public class ConfirmOrganizationUserCommandTests
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||
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 DeleteClaimedOrganizationUserAccountCommandvNextTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteUserAsync_WithValidSingleUser_CallsDeleteManyUsersAsync(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> 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<DeleteClaimedOrganizationUserAccountCommandvNext> 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<DeleteClaimedOrganizationUserAccountCommandvNext> 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<DeleteClaimedOrganizationUserAccountCommandvNext> 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<DeleteClaimedOrganizationUserAccountCommandvNext> 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<DeleteClaimedOrganizationUserAccountCommandvNext> 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<DeleteClaimedOrganizationUserAccountCommandvNext>>()
|
||||
.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<DeleteClaimedOrganizationUserAccountCommandvNext> 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<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
|
||||
.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<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
|
||||
.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<DeleteClaimedOrganizationUserAccountCommandvNext> 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<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
|
||||
.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<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
|
||||
.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<DeleteClaimedOrganizationUserAccountCommandvNext> 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<DeleteClaimedOrganizationUserAccountCommandvNext> sutProvider,
|
||||
IEnumerable<ValidationResult<DeleteUserValidationRequest>> validationResults)
|
||||
{
|
||||
sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidatorvNext>()
|
||||
.ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())
|
||||
.Returns(validationResults);
|
||||
}
|
||||
|
||||
private static async Task AssertSuccessfulUserOperations(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountCommandvNext> 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<DeleteClaimedOrganizationUserAccountCommandvNext> 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.DeleteClaimedAccountvNext;
|
||||
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 DeleteClaimedOrganizationUserAccountValidatorvNextTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithValidSingleRequest_ReturnsValidResult(
|
||||
SutProvider<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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<DeleteClaimedOrganizationUserAccountValidatorvNext> 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,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
{
|
||||
private const string _defaultUserCollectionName = "Default";
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ExecuteSideEffectsAsync_FeatureFlagDisabled_DoesNothing(
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
|
||||
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(false);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ExecuteSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing(
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy previousPolicyState,
|
||||
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ExecuteSideEffectsAsync_PolicyBeingDisabled_DoesNothing(
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
|
||||
[Policy(PolicyType.OrganizationDataOwnership)] Policy previousPolicyState,
|
||||
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ExecuteSideEffectsAsync_WhenNoUsersExist_DoNothing(
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
|
||||
OrganizationDataOwnershipPolicyRequirementFactory factory)
|
||||
{
|
||||
// Arrange
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
var policyRepository = ArrangePolicyRepository([]);
|
||||
var collectionRepository = Substitute.For<ICollectionRepository>();
|
||||
|
||||
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
await collectionRepository
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(
|
||||
Arg.Any<Guid>(),
|
||||
Arg.Any<IEnumerable<Guid>>(),
|
||||
Arg.Any<string>());
|
||||
|
||||
await policyRepository
|
||||
.Received(1)
|
||||
.GetPolicyDetailsByOrganizationIdAsync(
|
||||
policyUpdate.OrganizationId,
|
||||
PolicyType.OrganizationDataOwnership);
|
||||
}
|
||||
|
||||
public static IEnumerable<object?[]> ShouldUpsertDefaultCollectionsTestCases()
|
||||
{
|
||||
yield return WithExistingPolicy();
|
||||
|
||||
yield return WithNoExistingPolicy();
|
||||
yield break;
|
||||
|
||||
object?[] WithExistingPolicy()
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var postUpdatedPolicy = new Policy
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true
|
||||
};
|
||||
var previousPolicyState = new Policy
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = organizationId,
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
return new object?[]
|
||||
{
|
||||
postUpdatedPolicy,
|
||||
previousPolicyState
|
||||
};
|
||||
}
|
||||
|
||||
object?[] WithNoExistingPolicy()
|
||||
{
|
||||
var postUpdatedPolicy = new Policy
|
||||
{
|
||||
OrganizationId = new Guid(),
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
const Policy previousPolicyState = null;
|
||||
|
||||
return new object?[]
|
||||
{
|
||||
postUpdatedPolicy,
|
||||
previousPolicyState
|
||||
};
|
||||
}
|
||||
}
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]
|
||||
public async Task ExecuteSideEffectsAsync_WithRequirements_ShouldUpsertDefaultCollections(
|
||||
Policy postUpdatedPolicy,
|
||||
Policy? previousPolicyState,
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
|
||||
[OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable<OrganizationPolicyDetails> orgPolicyDetails,
|
||||
OrganizationDataOwnershipPolicyRequirementFactory factory)
|
||||
{
|
||||
// Arrange
|
||||
var orgPolicyDetailsList = orgPolicyDetails.ToList();
|
||||
foreach (var policyDetail in orgPolicyDetailsList)
|
||||
{
|
||||
policyDetail.OrganizationId = policyUpdate.OrganizationId;
|
||||
}
|
||||
|
||||
var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);
|
||||
var collectionRepository = Substitute.For<ICollectionRepository>();
|
||||
|
||||
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
await collectionRepository
|
||||
.Received(1)
|
||||
.UpsertDefaultCollectionsAsync(
|
||||
policyUpdate.OrganizationId,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3),
|
||||
_defaultUserCollectionName);
|
||||
}
|
||||
|
||||
private static IEnumerable<object?[]> WhenDefaultCollectionsDoesNotExistTestCases()
|
||||
{
|
||||
yield return [new OrganizationModelOwnershipPolicyModel(null)];
|
||||
yield return [new OrganizationModelOwnershipPolicyModel("")];
|
||||
yield return [new OrganizationModelOwnershipPolicyModel(" ")];
|
||||
yield return [new EmptyMetadataModel()];
|
||||
}
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(WhenDefaultCollectionsDoesNotExistTestCases))]
|
||||
public async Task ExecuteSideEffectsAsync_WhenDefaultCollectionNameIsInvalid_DoesNothing(
|
||||
IPolicyMetadataModel metadata,
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
|
||||
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
policyUpdate.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, null, metadata);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
private static IPolicyRepository ArrangePolicyRepository(IEnumerable<OrganizationPolicyDetails> policyDetails)
|
||||
{
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
|
||||
policyRepository
|
||||
.GetPolicyDetailsByOrganizationIdAsync(Arg.Any<Guid>(), PolicyType.OrganizationDataOwnership)
|
||||
.Returns(policyDetails);
|
||||
return policyRepository;
|
||||
}
|
||||
|
||||
private static OrganizationDataOwnershipPolicyValidator ArrangeSut(
|
||||
OrganizationDataOwnershipPolicyRequirementFactory factory,
|
||||
IPolicyRepository policyRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService);
|
||||
return sut;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationPolicyValidatorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithNoFactory_ThrowsNotImplementedException(
|
||||
Guid organizationId,
|
||||
SutProvider<TestOrganizationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), []);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotImplementedException>(() =>
|
||||
sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(
|
||||
organizationId, PolicyType.TwoFactorAuthentication));
|
||||
|
||||
Assert.Contains("No Requirement Factory found for", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithMultipleUsers_GroupsByUserId(
|
||||
Guid organizationId,
|
||||
Guid userId1,
|
||||
Guid userId2,
|
||||
SutProvider<TestOrganizationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var policyDetails = new List<OrganizationPolicyDetails>
|
||||
{
|
||||
new() { UserId = userId1, OrganizationId = organizationId },
|
||||
new() { UserId = userId1, OrganizationId = Guid.NewGuid() },
|
||||
new() { UserId = userId2, OrganizationId = organizationId }
|
||||
};
|
||||
|
||||
var factory = Substitute.For<IPolicyRequirementFactory<TestPolicyRequirement>>();
|
||||
factory.Create(Arg.Any<IEnumerable<PolicyDetails>>()).Returns(new TestPolicyRequirement());
|
||||
factory.Enforce(Arg.Any<PolicyDetails>()).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication)
|
||||
.Returns(policyDetails);
|
||||
|
||||
var factories = new List<IPolicyRequirementFactory<IPolicyRequirement>> { factory };
|
||||
var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), factories);
|
||||
|
||||
// Act
|
||||
var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(
|
||||
organizationId, PolicyType.TwoFactorAuthentication);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count());
|
||||
|
||||
factory.Received(2).Create(Arg.Any<IEnumerable<OrganizationPolicyDetails>>());
|
||||
factory.Received(1).Create(Arg.Is<IEnumerable<OrganizationPolicyDetails>>(
|
||||
results => results.Count() == 1 && results.First().UserId == userId2));
|
||||
factory.Received(1).Create(Arg.Is<IEnumerable<OrganizationPolicyDetails>>(
|
||||
results => results.Count() == 2 && results.First().UserId == userId1));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetUserPolicyRequirementsByOrganizationIdAsync_ShouldEnforceFilters(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
SutProvider<TestOrganizationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var adminUser = new OrganizationPolicyDetails()
|
||||
{
|
||||
UserId = userId,
|
||||
OrganizationId = organizationId,
|
||||
OrganizationUserType = OrganizationUserType.Admin
|
||||
};
|
||||
|
||||
var user = new OrganizationPolicyDetails()
|
||||
{
|
||||
UserId = userId,
|
||||
OrganizationId = organizationId,
|
||||
OrganizationUserType = OrganizationUserType.User
|
||||
};
|
||||
|
||||
var policyDetails = new List<OrganizationPolicyDetails>
|
||||
{
|
||||
adminUser,
|
||||
user
|
||||
};
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication)
|
||||
.Returns(policyDetails);
|
||||
|
||||
var factory = Substitute.For<IPolicyRequirementFactory<TestPolicyRequirement>>();
|
||||
factory.Create(Arg.Any<IEnumerable<PolicyDetails>>()).Returns(new TestPolicyRequirement());
|
||||
factory.Enforce(Arg.Is<PolicyDetails>(p => p.OrganizationUserType == OrganizationUserType.Admin))
|
||||
.Returns(true);
|
||||
factory.Enforce(Arg.Is<PolicyDetails>(p => p.OrganizationUserType == OrganizationUserType.User))
|
||||
.Returns(false);
|
||||
|
||||
var factories = new List<IPolicyRequirementFactory<IPolicyRequirement>> { factory };
|
||||
var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), factories);
|
||||
|
||||
// Act
|
||||
var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(
|
||||
organizationId, PolicyType.TwoFactorAuthentication);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
|
||||
factory.Received(1).Create(Arg.Is<IEnumerable<PolicyDetails>>(policies =>
|
||||
policies.Count() == 1 && policies.First().OrganizationUserType == OrganizationUserType.Admin));
|
||||
|
||||
factory.Received(1).Enforce(Arg.Is<PolicyDetails>(p => ReferenceEquals(p, adminUser)));
|
||||
factory.Received(1).Enforce(Arg.Is<PolicyDetails>(p => ReferenceEquals(p, user)));
|
||||
factory.Received(2).Enforce(Arg.Any<OrganizationPolicyDetails>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithEmptyPolicyDetails_ReturnsEmptyCollection(
|
||||
Guid organizationId,
|
||||
SutProvider<TestOrganizationPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var factory = Substitute.For<IPolicyRequirementFactory<TestPolicyRequirement>>();
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication)
|
||||
.Returns(new List<OrganizationPolicyDetails>());
|
||||
|
||||
var factories = new List<IPolicyRequirementFactory<IPolicyRequirement>> { factory };
|
||||
var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), factories);
|
||||
|
||||
// Act
|
||||
var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(
|
||||
organizationId, PolicyType.TwoFactorAuthentication);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
factory.DidNotReceive().Create(Arg.Any<IEnumerable<PolicyDetails>>());
|
||||
}
|
||||
}
|
||||
|
||||
public class TestOrganizationPolicyValidator : OrganizationPolicyValidator
|
||||
{
|
||||
public TestOrganizationPolicyValidator(
|
||||
IPolicyRepository policyRepository,
|
||||
IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>>? factories = null)
|
||||
: base(policyRepository, factories ?? [])
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<T>> TestGetUserPolicyRequirementsByOrganizationIdAsync<T>(Guid organizationId, PolicyType policyType)
|
||||
where T : IPolicyRequirement
|
||||
{
|
||||
return await GetUserPolicyRequirementsByOrganizationIdAsync<T>(organizationId, policyType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TestPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
}
|
||||
@@ -94,8 +94,8 @@ public class SavePolicyCommandTests
|
||||
Substitute.For<IEventService>(),
|
||||
Substitute.For<IPolicyRepository>(),
|
||||
[new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()],
|
||||
Substitute.For<TimeProvider>()
|
||||
));
|
||||
Substitute.For<TimeProvider>(),
|
||||
Substitute.For<IPostSavePolicySideEffect>()));
|
||||
Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message);
|
||||
}
|
||||
|
||||
@@ -281,6 +281,85 @@ public class SavePolicyCommandTests
|
||||
await AssertPolicyNotSavedAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task VNextSaveAsync_OrganizationDataOwnershipPolicy_ExecutesPostSaveSideEffects(
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = SutProviderFactory();
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||
.Returns(currentPolicy);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(result);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogPolicyEventAsync(result, EventType.Policy_Updated);
|
||||
|
||||
await sutProvider.GetDependency<IPostSavePolicySideEffect>()
|
||||
.Received(1)
|
||||
.ExecuteSideEffectsAsync(savePolicyModel, result, currentPolicy);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PolicyType.SingleOrg)]
|
||||
[BitAutoData(PolicyType.TwoFactorAuthentication)]
|
||||
public async Task VNextSaveAsync_NonOrganizationDataOwnershipPolicy_DoesNotExecutePostSaveSideEffects(
|
||||
PolicyType policyType,
|
||||
Policy currentPolicy,
|
||||
[PolicyUpdate] PolicyUpdate policyUpdate)
|
||||
{
|
||||
// Arrange
|
||||
policyUpdate.Type = policyType;
|
||||
currentPolicy.Type = policyType;
|
||||
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
|
||||
var sutProvider = SutProviderFactory();
|
||||
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||
.Returns(currentPolicy);
|
||||
|
||||
ArrangeOrganization(sutProvider, policyUpdate);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||
.Returns([currentPolicy]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
.Received(1)
|
||||
.UpsertAsync(result);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogPolicyEventAsync(result, EventType.Policy_Updated);
|
||||
|
||||
await sutProvider.GetDependency<IPostSavePolicySideEffect>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.ExecuteSideEffectsAsync(default!, default!, default!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new SutProvider with the PolicyValidators registered in the Sut.
|
||||
/// </summary>
|
||||
@@ -289,6 +368,7 @@ public class SavePolicyCommandTests
|
||||
return new SutProvider<SavePolicyCommand>()
|
||||
.WithFakeTimeProvider()
|
||||
.SetDependency(policyValidators ?? [])
|
||||
.SetDependency(Substitute.For<IPostSavePolicySideEffect>())
|
||||
.Create();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Net;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Bit.Test.Common.MockedHttpClient;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DatadogIntegrationHandlerTests
|
||||
{
|
||||
private readonly MockedHttpMessageHandler _handler;
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string _apiKey = "AUTH_TOKEN";
|
||||
private static readonly Uri _datadogUri = new Uri("https://localhost");
|
||||
|
||||
public DatadogIntegrationHandlerTests()
|
||||
{
|
||||
_handler = new MockedHttpMessageHandler();
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
_httpClient = _handler.ToHttpClient();
|
||||
}
|
||||
|
||||
private SutProvider<DatadogIntegrationHandler> GetSutProvider()
|
||||
{
|
||||
var clientFactory = Substitute.For<IHttpClientFactory>();
|
||||
clientFactory.CreateClient(DatadogIntegrationHandler.HttpClientName).Returns(_httpClient);
|
||||
|
||||
return new SutProvider<DatadogIntegrationHandler>()
|
||||
.SetDependency(clientFactory)
|
||||
.WithFakeTimeProvider()
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.Empty(result.FailureReason);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(DatadogIntegrationHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
Assert.NotNull(request.Content);
|
||||
var returned = await request.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_apiKey, request.Headers.GetValues("DD-API-KEY").Single());
|
||||
Assert.Equal(_datadogUri, request.RequestUri);
|
||||
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
|
||||
var retryAfter = now.AddSeconds(60);
|
||||
|
||||
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);
|
||||
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TooManyRequests)
|
||||
.WithHeader("Retry-After", "60")
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.True(result.DelayUntilDate.HasValue);
|
||||
Assert.Equal(retryAfter, result.DelayUntilDate.Value);
|
||||
Assert.Equal("Too Many Requests", result.FailureReason);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
|
||||
var retryAfter = now.AddSeconds(60);
|
||||
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TooManyRequests)
|
||||
.WithHeader("Retry-After", retryAfter.ToString("r"))
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.True(result.DelayUntilDate.HasValue);
|
||||
Assert.Equal(retryAfter, result.DelayUntilDate.Value);
|
||||
Assert.Equal("Too Many Requests", result.FailureReason);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.InternalServerError)
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.False(result.DelayUntilDate.HasValue);
|
||||
Assert.Equal("Internal Server Error", result.FailureReason);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TemporaryRedirect)
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.Null(result.DelayUntilDate);
|
||||
Assert.Equal("Temporary Redirect", result.FailureReason);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
@@ -27,7 +28,6 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Fakes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using NSubstitute.ReceivedExtensions;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -42,8 +42,6 @@ public class OrganizationServiceTests
|
||||
{
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
|
||||
|
||||
|
||||
|
||||
[Theory]
|
||||
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
|
||||
InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize, BitAutoData]
|
||||
@@ -1229,6 +1227,109 @@ public class OrganizationServiceTests
|
||||
.GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(false, true, false, true)]
|
||||
[BitAutoData(true, false, true, false)]
|
||||
public async Task UpdateCollectionManagementSettingsAsync_WhenSettingsChanged_LogsSpecificEvents(
|
||||
bool newLimitCollectionCreation,
|
||||
bool newLimitCollectionDeletion,
|
||||
bool newLimitItemDeletion,
|
||||
bool newAllowAdminAccessToAllCollectionItems,
|
||||
Organization existingOrganization, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
existingOrganization.LimitCollectionCreation = false;
|
||||
existingOrganization.LimitCollectionDeletion = false;
|
||||
existingOrganization.LimitItemDeletion = false;
|
||||
existingOrganization.AllowAdminAccessToAllCollectionItems = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(existingOrganization.Id)
|
||||
.Returns(existingOrganization);
|
||||
|
||||
var settings = new OrganizationCollectionManagementSettings
|
||||
{
|
||||
LimitCollectionCreation = newLimitCollectionCreation,
|
||||
LimitCollectionDeletion = newLimitCollectionDeletion,
|
||||
LimitItemDeletion = newLimitItemDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = newAllowAdminAccessToAllCollectionItems
|
||||
};
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateCollectionManagementSettingsAsync(existingOrganization.Id, settings);
|
||||
|
||||
// Assert
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
if (newLimitCollectionCreation)
|
||||
{
|
||||
await eventService.Received(1).LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
await eventService.DidNotReceive().LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled));
|
||||
}
|
||||
|
||||
if (newLimitCollectionDeletion)
|
||||
{
|
||||
await eventService.Received(1).LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
await eventService.DidNotReceive().LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled));
|
||||
}
|
||||
|
||||
if (newLimitItemDeletion)
|
||||
{
|
||||
await eventService.Received(1).LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitItemDeletionEnabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
await eventService.DidNotReceive().LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitItemDeletionEnabled));
|
||||
}
|
||||
|
||||
if (newAllowAdminAccessToAllCollectionItems)
|
||||
{
|
||||
await eventService.Received(1).LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
await eventService.DidNotReceive().LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateCollectionManagementSettingsAsync_WhenOrganizationNotFound_ThrowsNotFoundException(
|
||||
Guid organizationId, OrganizationCollectionManagementSettings settings, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
// Act/Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateCollectionManagementSettingsAsync(organizationId, settings));
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.Received(1)
|
||||
.GetByIdAsync(organizationId);
|
||||
}
|
||||
|
||||
// Must set real guids in order for dictionary of guids to not throw aggregate exceptions
|
||||
private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
|
||||
@@ -51,6 +51,7 @@ public class WebhookIntegrationHandlerTests
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.Empty(result.FailureReason);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
|
||||
@@ -59,6 +60,7 @@ public class WebhookIntegrationHandlerTests
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
Assert.NotNull(request.Content);
|
||||
var returned = await request.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
@@ -77,6 +79,7 @@ public class WebhookIntegrationHandlerTests
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.Empty(result.FailureReason);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
|
||||
@@ -85,6 +88,7 @@ public class WebhookIntegrationHandlerTests
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
Assert.NotNull(request.Content);
|
||||
var returned = await request.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -41,22 +40,12 @@ public class IntegrationTemplateProcessorTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ReplaceTokens_WithEventMessageToken_ReplacesWithSerializedJson(EventMessage eventMessage)
|
||||
{
|
||||
var template = "#EventMessage#";
|
||||
var expected = $"{JsonSerializer.Serialize(eventMessage)}";
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ReplaceTokens_WithNullProperty_LeavesTokenUnchanged(EventMessage eventMessage)
|
||||
public void ReplaceTokens_WithNullProperty_InsertsEmptyString(EventMessage eventMessage)
|
||||
{
|
||||
eventMessage.UserId = null;
|
||||
|
||||
var template = "Event #Type#, User (id: #UserId#).";
|
||||
var expected = $"Event {eventMessage.Type}, User (id: #UserId#).";
|
||||
var expected = $"Event {eventMessage.Type}, User (id: ).";
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.UserFeatures.SendAccess;
|
||||
using Bit.Core.Identity;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.UserFeatures.SendAccess;
|
||||
@@ -12,7 +12,7 @@ public class SendAccessClaimsPrincipalExtensionsTests
|
||||
{
|
||||
// Arrange
|
||||
var guid = Guid.NewGuid();
|
||||
var claims = new[] { new Claim(Claims.SendId, guid.ToString()) };
|
||||
var claims = new[] { new Claim(Claims.SendAccessClaims.SendId, guid.ToString()) };
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
@@ -30,19 +30,19 @@ public class SendAccessClaimsPrincipalExtensionsTests
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => principal.GetSendId());
|
||||
Assert.Equal("Send ID claim not found.", ex.Message);
|
||||
Assert.Equal("send_id claim not found.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSendId_ThrowsInvalidOperationException_WhenClaimValueIsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var claims = new[] { new Claim(Claims.SendId, "not-a-guid") };
|
||||
var claims = new[] { new Claim(Claims.SendAccessClaims.SendId, "not-a-guid") };
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => principal.GetSendId());
|
||||
Assert.Equal("Invalid Send ID claim value.", ex.Message);
|
||||
Assert.Equal("Invalid send_id claim value.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
394
test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs
Normal file
394
test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs
Normal file
@@ -0,0 +1,394 @@
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Extensions;
|
||||
|
||||
public class InvoiceExtensionsTests
|
||||
{
|
||||
private static Invoice CreateInvoiceWithLines(params InvoiceLineItem[] lineItems)
|
||||
{
|
||||
return new Invoice
|
||||
{
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = lineItems?.ToList() ?? new List<InvoiceLineItem>()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#region FormatForProvider Tests
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_NullLines_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = new Invoice
|
||||
{
|
||||
Lines = null
|
||||
};
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_EmptyLines_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines();
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_NullLineItem_SkipsNullLine()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(null);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_LineWithNullDescription_SkipsLine()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem { Description = null, Quantity = 1, Amount = 1000 }
|
||||
);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_ProviderPortalTeams_FormatsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Provider Portal - Teams (at $6.00 / month)",
|
||||
Quantity = 5,
|
||||
Amount = 3000
|
||||
}
|
||||
);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("5 × Manage service provider (at $6.00 / month)", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_ProviderPortalEnterprise_FormatsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Provider Portal - Enterprise (at $4.00 / month)",
|
||||
Quantity = 10,
|
||||
Amount = 4000
|
||||
}
|
||||
);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("10 × Manage service provider (at $4.00 / month)", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_ProviderPortalWithoutPriceInfo_FormatsWithoutPrice()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Provider Portal - Teams",
|
||||
Quantity = 3,
|
||||
Amount = 1800
|
||||
}
|
||||
);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("3 × Manage service provider ", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_BusinessUnitPortalEnterprise_FormatsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Business Unit Portal - Enterprise (at $5.00 / month)",
|
||||
Quantity = 8,
|
||||
Amount = 4000
|
||||
}
|
||||
);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("8 × Manage service provider (at $5.00 / month)", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_BusinessUnitPortalGeneric_FormatsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Business Unit Portal (at $3.00 / month)",
|
||||
Quantity = 2,
|
||||
Amount = 600
|
||||
}
|
||||
);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("2 × Manage service provider (at $3.00 / month)", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_TaxLineWithPriceInfo_FormatsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Tax (at $2.00 / month)",
|
||||
Quantity = 1,
|
||||
Amount = 200
|
||||
}
|
||||
);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("1 × Tax (at $2.00 / month)", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_TaxLineWithoutPriceInfo_CalculatesPrice()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Tax",
|
||||
Quantity = 2,
|
||||
Amount = 400 // $4.00 total, $2.00 per item
|
||||
}
|
||||
);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("2 × Tax (at $2.00 / month)", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_TaxLineWithZeroQuantity_DoesNotCalculatePrice()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Tax",
|
||||
Quantity = 0,
|
||||
Amount = 200
|
||||
}
|
||||
);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("0 × Tax ", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_OtherLineItem_ReturnsAsIs()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Some other service",
|
||||
Quantity = 1,
|
||||
Amount = 1000
|
||||
}
|
||||
);
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Some other service", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_InvoiceLevelTax_AddsToResult()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Provider Portal - Teams",
|
||||
Quantity = 1,
|
||||
Amount = 600
|
||||
}
|
||||
);
|
||||
invoice.Tax = 120; // $1.20 in cents
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("1 × Manage service provider ", result[0]);
|
||||
Assert.Equal("1 × Tax (at $1.20 / month)", result[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_NoInvoiceLevelTax_DoesNotAddTax()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Provider Portal - Teams",
|
||||
Quantity = 1,
|
||||
Amount = 600
|
||||
}
|
||||
);
|
||||
invoice.Tax = null;
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("1 × Manage service provider ", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_ZeroInvoiceLevelTax_DoesNotAddTax()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = CreateInvoiceWithLines(
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Provider Portal - Teams",
|
||||
Quantity = 1,
|
||||
Amount = 600
|
||||
}
|
||||
);
|
||||
invoice.Tax = 0;
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("1 × Manage service provider ", result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatForProvider_ComplexScenario_HandlesAllLineTypes()
|
||||
{
|
||||
// Arrange
|
||||
var lineItems = new StripeList<InvoiceLineItem>();
|
||||
lineItems.Data = new List<InvoiceLineItem>
|
||||
{
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Provider Portal - Teams (at $6.00 / month)", Quantity = 5, Amount = 3000
|
||||
},
|
||||
new InvoiceLineItem
|
||||
{
|
||||
Description = "Provider Portal - Enterprise (at $4.00 / month)", Quantity = 10, Amount = 4000
|
||||
},
|
||||
new InvoiceLineItem { Description = "Tax", Quantity = 1, Amount = 800 },
|
||||
new InvoiceLineItem { Description = "Custom Service", Quantity = 2, Amount = 2000 }
|
||||
};
|
||||
|
||||
var invoice = new Invoice
|
||||
{
|
||||
Lines = lineItems,
|
||||
Tax = 200 // Additional $2.00 tax
|
||||
};
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, result.Count);
|
||||
Assert.Equal("5 × Manage service provider (at $6.00 / month)", result[0]);
|
||||
Assert.Equal("10 × Manage service provider (at $4.00 / month)", result[1]);
|
||||
Assert.Equal("1 × Tax (at $8.00 / month)", result[2]);
|
||||
Assert.Equal("Custom Service", result[3]);
|
||||
Assert.Equal("1 × Tax (at $2.00 / month)", result[4]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -71,7 +71,7 @@ public class GetOrganizationWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
|
||||
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns((string?)null);
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(organization.Id).Returns((string?)null);
|
||||
|
||||
var response = await sutProvider.Sut.Run(organization);
|
||||
|
||||
@@ -109,7 +109,7 @@ public class GetOrganizationWarningsQueryTests
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
|
||||
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns(setupIntentId);
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(organization.Id).Returns(setupIntentId);
|
||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(
|
||||
options => options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
||||
{
|
||||
|
||||
@@ -74,7 +74,10 @@ public class UpdatePaymentMethodCommandTests
|
||||
},
|
||||
NextAction = new SetupIntentNextAction
|
||||
{
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
|
||||
{
|
||||
HostedVerificationUrl = "https://example.com"
|
||||
}
|
||||
},
|
||||
Status = "requires_action"
|
||||
};
|
||||
@@ -95,7 +98,7 @@ public class UpdatePaymentMethodCommandTests
|
||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||
Assert.False(maskedBankAccount.Verified);
|
||||
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
|
||||
|
||||
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
|
||||
}
|
||||
@@ -133,7 +136,10 @@ public class UpdatePaymentMethodCommandTests
|
||||
},
|
||||
NextAction = new SetupIntentNextAction
|
||||
{
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
|
||||
{
|
||||
HostedVerificationUrl = "https://example.com"
|
||||
}
|
||||
},
|
||||
Status = "requires_action"
|
||||
};
|
||||
@@ -154,7 +160,7 @@ public class UpdatePaymentMethodCommandTests
|
||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||
Assert.False(maskedBankAccount.Verified);
|
||||
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
|
||||
|
||||
await _subscriberService.Received(1).CreateStripeCustomer(organization);
|
||||
|
||||
@@ -199,7 +205,10 @@ public class UpdatePaymentMethodCommandTests
|
||||
},
|
||||
NextAction = new SetupIntentNextAction
|
||||
{
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
|
||||
{
|
||||
HostedVerificationUrl = "https://example.com"
|
||||
}
|
||||
},
|
||||
Status = "requires_action"
|
||||
};
|
||||
@@ -220,7 +229,7 @@ public class UpdatePaymentMethodCommandTests
|
||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||
Assert.False(maskedBankAccount.Verified);
|
||||
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
|
||||
|
||||
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
|
||||
await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Payment.Commands;
|
||||
|
||||
public class VerifyBankAccountCommandTests
|
||||
{
|
||||
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly VerifyBankAccountCommand _command;
|
||||
|
||||
public VerifyBankAccountCommandTests()
|
||||
{
|
||||
_command = new VerifyBankAccountCommand(
|
||||
Substitute.For<ILogger<VerifyBankAccountCommand>>(),
|
||||
_setupIntentCache,
|
||||
_stripeAdapter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_MakesCorrectInvocations_ReturnsMaskedBankAccount()
|
||||
{
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
GatewayCustomerId = "cus_123"
|
||||
};
|
||||
|
||||
const string setupIntentId = "seti_123";
|
||||
|
||||
_setupIntentCache.Get(organization.Id).Returns(setupIntentId);
|
||||
|
||||
var setupIntent = new SetupIntent
|
||||
{
|
||||
Id = setupIntentId,
|
||||
PaymentMethodId = "pm_123",
|
||||
PaymentMethod =
|
||||
new PaymentMethod
|
||||
{
|
||||
Id = "pm_123",
|
||||
Type = "us_bank_account",
|
||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
||||
},
|
||||
NextAction = new SetupIntentNextAction
|
||||
{
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||
},
|
||||
Status = "requires_action"
|
||||
};
|
||||
|
||||
_stripeAdapter.SetupIntentGet(setupIntentId,
|
||||
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method"))).Returns(setupIntent);
|
||||
|
||||
_stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
|
||||
Arg.Is<PaymentMethodAttachOptions>(options => options.Customer == organization.GatewayCustomerId))
|
||||
.Returns(setupIntent.PaymentMethod);
|
||||
|
||||
var result = await _command.Run(organization, "DESCRIPTOR_CODE");
|
||||
|
||||
Assert.True(result.IsT0);
|
||||
var maskedPaymentMethod = result.AsT0;
|
||||
Assert.True(maskedPaymentMethod.IsT0);
|
||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||
Assert.True(maskedBankAccount.Verified);
|
||||
|
||||
await _stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id,
|
||||
Arg.Is<SetupIntentVerifyMicrodepositsOptions>(options => options.DescriptorCode == "DESCRIPTOR_CODE"));
|
||||
|
||||
await _stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
||||
options => options.InvoiceSettings.DefaultPaymentMethod == setupIntent.PaymentMethodId));
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public class MaskedPaymentMethodTests
|
||||
{
|
||||
BankName = "Chase",
|
||||
Last4 = "9999",
|
||||
Verified = true
|
||||
HostedVerificationUrl = "https://example.com"
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(input);
|
||||
@@ -32,7 +32,7 @@ public class MaskedPaymentMethodTests
|
||||
{
|
||||
BankName = "Chase",
|
||||
Last4 = "9999",
|
||||
Verified = true
|
||||
HostedVerificationUrl = "https://example.com"
|
||||
};
|
||||
|
||||
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
|
||||
@@ -108,7 +108,7 @@ public class GetPaymentMethodQueryTests
|
||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||
Assert.True(maskedBankAccount.Verified);
|
||||
Assert.Null(maskedBankAccount.HostedVerificationUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -142,7 +142,7 @@ public class GetPaymentMethodQueryTests
|
||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||
Assert.True(maskedBankAccount.Verified);
|
||||
Assert.Null(maskedBankAccount.HostedVerificationUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -163,7 +163,7 @@ public class GetPaymentMethodQueryTests
|
||||
Arg.Is<CustomerGetOptions>(options =>
|
||||
options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer);
|
||||
|
||||
_setupIntentCache.Get(organization.Id).Returns("seti_123");
|
||||
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
|
||||
|
||||
_stripeAdapter
|
||||
.SetupIntentGet("seti_123",
|
||||
@@ -177,7 +177,10 @@ public class GetPaymentMethodQueryTests
|
||||
},
|
||||
NextAction = new SetupIntentNextAction
|
||||
{
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
|
||||
{
|
||||
HostedVerificationUrl = "https://example.com"
|
||||
}
|
||||
},
|
||||
Status = "requires_action"
|
||||
});
|
||||
@@ -189,7 +192,7 @@ public class GetPaymentMethodQueryTests
|
||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||
Assert.False(maskedBankAccount.Verified);
|
||||
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Braintree;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
using Address = Stripe.Address;
|
||||
using StripeCustomer = Stripe.Customer;
|
||||
using StripeSubscription = Stripe.Subscription;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Premium.Commands;
|
||||
|
||||
public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
{
|
||||
private readonly IBraintreeGateway _braintreeGateway = Substitute.For<IBraintreeGateway>();
|
||||
private readonly IGlobalSettings _globalSettings = Substitute.For<IGlobalSettings>();
|
||||
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
|
||||
private readonly IUserService _userService = Substitute.For<IUserService>();
|
||||
private readonly IPushNotificationService _pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||
private readonly CreatePremiumCloudHostedSubscriptionCommand _command;
|
||||
|
||||
public CreatePremiumCloudHostedSubscriptionCommandTests()
|
||||
{
|
||||
var baseServiceUri = Substitute.For<IBaseServiceUriSettings>();
|
||||
baseServiceUri.CloudRegion.Returns("US");
|
||||
_globalSettings.BaseServiceUri.Returns(baseServiceUri);
|
||||
|
||||
_command = new CreatePremiumCloudHostedSubscriptionCommand(
|
||||
_braintreeGateway,
|
||||
_globalSettings,
|
||||
_setupIntentCache,
|
||||
_stripeAdapter,
|
||||
_subscriberService,
|
||||
_userService,
|
||||
_pushNotificationService,
|
||||
Substitute.For<ILogger<CreatePremiumCloudHostedSubscriptionCommand>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_UserAlreadyPremium_ReturnsBadRequest(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("Already a premium user.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NegativeStorageAmount_ReturnsBadRequest(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, -1);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("Additional storage must be greater than 0.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_ValidPaymentMethodTypes_BankAccount_Success(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null; // Ensure no existing customer ID
|
||||
user.Email = "test@example.com";
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.BankAccount;
|
||||
paymentMethod.Token = "bank_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active";
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
var mockSetupIntent = Substitute.For<SetupIntent>();
|
||||
mockSetupIntent.Id = "seti_123";
|
||||
|
||||
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
_stripeAdapter.SetupIntentList(Arg.Any<SetupIntentListOptions>()).Returns(Task.FromResult(new List<SetupIntent> { mockSetupIntent }));
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
|
||||
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
await _userService.Received(1).SaveUserAsync(user);
|
||||
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_ValidPaymentMethodTypes_Card_Success(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null;
|
||||
user.Email = "test@example.com";
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.Card;
|
||||
paymentMethod.Token = "card_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active";
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
|
||||
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
await _userService.Received(1).SaveUserAsync(user);
|
||||
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_ValidPaymentMethodTypes_PayPal_Success(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null;
|
||||
user.Email = "test@example.com";
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.PayPal;
|
||||
paymentMethod.Token = "paypal_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active";
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
_subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns("bt_customer_123");
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
|
||||
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
await _subscriberService.Received(1).CreateBraintreeCustomer(user, paymentMethod.Token);
|
||||
await _userService.Received(1).SaveUserAsync(user);
|
||||
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_ValidRequestWithAdditionalStorage_Success(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null;
|
||||
user.Email = "test@example.com";
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.Card;
|
||||
paymentMethod.Token = "card_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
const short additionalStorage = 2;
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active";
|
||||
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, additionalStorage);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
Assert.True(user.Premium);
|
||||
Assert.Equal((short)(1 + additionalStorage), user.MaxStorageGb);
|
||||
Assert.NotNull(user.LicenseKey);
|
||||
Assert.Equal(20, user.LicenseKey.Length);
|
||||
Assert.NotEqual(default, user.RevisionDate);
|
||||
await _userService.Received(1).SaveUserAsync(user);
|
||||
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_UserHasExistingGatewayCustomerId_UsesExistingCustomer(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = "existing_customer_123";
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.Card;
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "existing_customer_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active";
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());
|
||||
await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_PayPalWithIncompleteSubscription_SetsPremiumTrue(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null;
|
||||
user.Email = "test@example.com";
|
||||
user.PremiumExpirationDate = null;
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.PayPal;
|
||||
paymentMethod.Token = "paypal_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "incomplete";
|
||||
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
_subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns("bt_customer_123");
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
Assert.True(user.Premium);
|
||||
Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NonPayPalWithActiveSubscription_SetsPremiumTrue(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null;
|
||||
user.Email = "test@example.com";
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.Card;
|
||||
paymentMethod.Token = "card_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active";
|
||||
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
Assert.True(user.Premium);
|
||||
Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_SubscriptionStatusDoesNotMatchPatterns_DoesNotSetPremium(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null;
|
||||
user.Email = "test@example.com";
|
||||
user.PremiumExpirationDate = null;
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.PayPal;
|
||||
paymentMethod.Token = "paypal_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active"; // PayPal + active doesn't match pattern
|
||||
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
_subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns("bt_customer_123");
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
Assert.False(user.Premium);
|
||||
Assert.Null(user.PremiumExpirationDate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_BankAccountWithNoSetupIntentFound_ReturnsUnhandled(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null;
|
||||
user.Email = "test@example.com";
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.BankAccount;
|
||||
paymentMethod.Token = "bank_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "incomplete";
|
||||
mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30);
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
|
||||
_stripeAdapter.SetupIntentList(Arg.Any<SetupIntentListOptions>())
|
||||
.Returns(Task.FromResult(new List<SetupIntent>())); // Empty list - no setup intent found
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT3);
|
||||
var unhandled = result.AsT3;
|
||||
Assert.Equal("Something went wrong with your request. Please contact support for assistance.", unhandled.Response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Premium.Commands;
|
||||
|
||||
public class CreatePremiumSelfHostedSubscriptionCommandTests
|
||||
{
|
||||
private readonly ILicensingService _licensingService = Substitute.For<ILicensingService>();
|
||||
private readonly IUserService _userService = Substitute.For<IUserService>();
|
||||
private readonly IPushNotificationService _pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||
private readonly CreatePremiumSelfHostedSubscriptionCommand _command;
|
||||
|
||||
public CreatePremiumSelfHostedSubscriptionCommandTests()
|
||||
{
|
||||
_command = new CreatePremiumSelfHostedSubscriptionCommand(
|
||||
_licensingService,
|
||||
_userService,
|
||||
_pushNotificationService,
|
||||
Substitute.For<ILogger<CreatePremiumSelfHostedSubscriptionCommand>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_UserAlreadyPremium_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Premium = true
|
||||
};
|
||||
|
||||
var license = new UserLicense
|
||||
{
|
||||
LicenseKey = "test_key",
|
||||
Expires = DateTime.UtcNow.AddYears(1)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, license);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("Already a premium user.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_InvalidLicense_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Premium = false
|
||||
};
|
||||
|
||||
var license = new UserLicense
|
||||
{
|
||||
LicenseKey = "invalid_key",
|
||||
Expires = DateTime.UtcNow.AddYears(1)
|
||||
};
|
||||
|
||||
_licensingService.VerifyLicense(license).Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, license);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("Invalid license.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_LicenseCannotBeUsed_EmailNotVerified_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Premium = false,
|
||||
Email = "test@example.com",
|
||||
EmailVerified = false
|
||||
};
|
||||
|
||||
var license = new UserLicense
|
||||
{
|
||||
LicenseKey = "test_key",
|
||||
Expires = DateTime.UtcNow.AddYears(1),
|
||||
Token = "valid_token"
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim("Email", "test@example.com")
|
||||
}));
|
||||
|
||||
_licensingService.VerifyLicense(license).Returns(true);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, license);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Contains("The user's email is not verified.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_LicenseCannotBeUsed_EmailMismatch_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Premium = false,
|
||||
Email = "user@example.com",
|
||||
EmailVerified = true
|
||||
};
|
||||
|
||||
var license = new UserLicense
|
||||
{
|
||||
LicenseKey = "test_key",
|
||||
Expires = DateTime.UtcNow.AddYears(1),
|
||||
Token = "valid_token"
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim("Email", "license@example.com")
|
||||
}));
|
||||
|
||||
_licensingService.VerifyLicense(license).Returns(true);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, license);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Contains("The user's email does not match the license email.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_ValidRequest_Success()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new User
|
||||
{
|
||||
Id = userId,
|
||||
Premium = false,
|
||||
Email = "test@example.com",
|
||||
EmailVerified = true
|
||||
};
|
||||
|
||||
var license = new UserLicense
|
||||
{
|
||||
LicenseKey = "test_key_12345",
|
||||
Expires = DateTime.UtcNow.AddYears(1),
|
||||
Token = "valid_token"
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim("Email", "test@example.com")
|
||||
}));
|
||||
|
||||
_licensingService.VerifyLicense(license).Returns(true);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, license);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
// Verify user was updated correctly
|
||||
Assert.True(user.Premium);
|
||||
Assert.NotNull(user.LicenseKey);
|
||||
Assert.Equal(license.LicenseKey, user.LicenseKey);
|
||||
Assert.NotEqual(default, user.RevisionDate);
|
||||
|
||||
// Verify services were called
|
||||
await _licensingService.Received(1).WriteUserLicenseAsync(user, license);
|
||||
await _userService.Received(1).SaveUserAsync(user);
|
||||
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Text.Json;
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.Billing.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@@ -16,6 +18,8 @@ public class LicensingServiceTests
|
||||
{
|
||||
private static string licenseFilePath(Guid orgId) =>
|
||||
Path.Combine(OrganizationLicenseDirectory.Value, $"{orgId}.json");
|
||||
private static string userLicenseFilePath(Guid userId) =>
|
||||
Path.Combine(UserLicenseDirectory.Value, $"{userId}.json");
|
||||
private static string LicenseDirectory => Path.GetDirectoryName(OrganizationLicenseDirectory.Value);
|
||||
private static Lazy<string> OrganizationLicenseDirectory => new(() =>
|
||||
{
|
||||
@@ -26,6 +30,15 @@ public class LicensingServiceTests
|
||||
}
|
||||
return directory;
|
||||
});
|
||||
private static Lazy<string> UserLicenseDirectory => new(() =>
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), "user");
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
return directory;
|
||||
});
|
||||
|
||||
public static SutProvider<LicensingService> GetSutProvider()
|
||||
{
|
||||
@@ -57,4 +70,66 @@ public class LicensingServiceTests
|
||||
Directory.Delete(OrganizationLicenseDirectory.Value, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task WriteUserLicense_CreatesFileWithCorrectContent(User user, UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = GetSutProvider();
|
||||
var expectedFilePath = userLicenseFilePath(user.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await sutProvider.Sut.WriteUserLicenseAsync(user, license);
|
||||
|
||||
// Assert
|
||||
Assert.True(File.Exists(expectedFilePath));
|
||||
var fileContent = await File.ReadAllTextAsync(expectedFilePath);
|
||||
var actualLicense = JsonSerializer.Deserialize<UserLicense>(fileContent);
|
||||
|
||||
Assert.Equal(license.LicenseKey, actualLicense.LicenseKey);
|
||||
Assert.Equal(license.Id, actualLicense.Id);
|
||||
Assert.Equal(license.Expires, actualLicense.Expires);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
if (Directory.Exists(UserLicenseDirectory.Value))
|
||||
{
|
||||
Directory.Delete(UserLicenseDirectory.Value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task WriteUserLicense_CreatesDirectoryIfNotExists(User user, UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
var sutProvider = GetSutProvider();
|
||||
|
||||
// Ensure directory doesn't exist
|
||||
if (Directory.Exists(UserLicenseDirectory.Value))
|
||||
{
|
||||
Directory.Delete(UserLicenseDirectory.Value, true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
await sutProvider.Sut.WriteUserLicenseAsync(user, license);
|
||||
|
||||
// Assert
|
||||
Assert.True(Directory.Exists(UserLicenseDirectory.Value));
|
||||
Assert.True(File.Exists(userLicenseFilePath(user.Id)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
if (Directory.Exists(UserLicenseDirectory.Value))
|
||||
{
|
||||
Directory.Delete(UserLicenseDirectory.Value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,13 +329,165 @@ public class SubscriberServiceTests
|
||||
#endregion
|
||||
|
||||
#region GetPaymentMethod
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_NullSubscriber_ThrowsArgumentNullException(
|
||||
SutProvider<SubscriberService> sutProvider) =>
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_Braintree_NoDefaultPaymentMethod_ReturnsNull(
|
||||
public async Task GetPaymentMethod_WithNegativeStripeAccountBalance_ReturnsCorrectAccountCreditAmount(Organization organization,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
// Stripe reports balance in cents as a negative number for credit
|
||||
const int stripeAccountBalance = -593; // $5.93 credit (negative cents)
|
||||
const decimal creditAmount = 5.93M; // Same value in dollars
|
||||
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Balance = stripeAccountBalance,
|
||||
Subscriptions = new StripeList<Subscription>()
|
||||
{
|
||||
Data =
|
||||
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
|
||||
},
|
||||
InvoiceSettings = new CustomerInvoiceSettings
|
||||
{
|
||||
DefaultPaymentMethod = new PaymentMethod
|
||||
{
|
||||
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
|
||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
||||
}
|
||||
}
|
||||
};
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")
|
||||
&& options.Expand.Contains("subscriptions")
|
||||
&& options.Expand.Contains("tax_ids")))
|
||||
.Returns(customer);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPaymentMethod(organization);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(creditAmount, result.AccountCredit);
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method") &&
|
||||
options.Expand.Contains("subscriptions") &&
|
||||
options.Expand.Contains("tax_ids")));
|
||||
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_WithZeroStripeAccountBalance_ReturnsCorrectAccountCreditAmount(
|
||||
Organization organization, SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const int stripeAccountBalance = 0;
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Balance = stripeAccountBalance,
|
||||
Subscriptions = new StripeList<Subscription>()
|
||||
{
|
||||
Data =
|
||||
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
|
||||
},
|
||||
InvoiceSettings = new CustomerInvoiceSettings
|
||||
{
|
||||
DefaultPaymentMethod = new PaymentMethod
|
||||
{
|
||||
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
|
||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
||||
}
|
||||
}
|
||||
};
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")
|
||||
&& options.Expand.Contains("subscriptions")
|
||||
&& options.Expand.Contains("tax_ids")))
|
||||
.Returns(customer);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPaymentMethod(organization);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0, result.AccountCredit);
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method") &&
|
||||
options.Expand.Contains("subscriptions") &&
|
||||
options.Expand.Contains("tax_ids")));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_WithPositiveStripeAccountBalance_ReturnsCorrectAccountCreditAmount(
|
||||
Organization organization, SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const int stripeAccountBalance = 593; // $5.93 charge balance
|
||||
const decimal accountBalance = -5.93M; // account balance
|
||||
var customer = new Customer
|
||||
{
|
||||
Balance = stripeAccountBalance,
|
||||
Subscriptions = new StripeList<Subscription>()
|
||||
{
|
||||
Data =
|
||||
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
|
||||
},
|
||||
InvoiceSettings = new CustomerInvoiceSettings
|
||||
{
|
||||
DefaultPaymentMethod = new PaymentMethod
|
||||
{
|
||||
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
|
||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
||||
}
|
||||
}
|
||||
};
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")
|
||||
&& options.Expand.Contains("subscriptions")
|
||||
&& options.Expand.Contains("tax_ids")))
|
||||
.Returns(customer);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPaymentMethod(organization);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(accountBalance, result.AccountCredit);
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method") &&
|
||||
options.Expand.Contains("subscriptions") &&
|
||||
options.Expand.Contains("tax_ids")));
|
||||
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GetPaymentSource
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentSource_NullSubscriber_ThrowsArgumentNullException(
|
||||
SutProvider<SubscriberService> sutProvider) =>
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentSource_Braintree_NoDefaultPaymentMethod_ReturnsNull(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
@@ -372,7 +524,7 @@ public class SubscriberServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_Braintree_PayPalAccount_Succeeds(
|
||||
public async Task GetPaymentSource_Braintree_PayPalAccount_Succeeds(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
@@ -421,7 +573,7 @@ public class SubscriberServiceTests
|
||||
// TODO: Determine if we need to test Braintree.UsBankAccount
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_Stripe_BankAccountPaymentMethod_Succeeds(
|
||||
public async Task GetPaymentSource_Stripe_BankAccountPaymentMethod_Succeeds(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
@@ -455,7 +607,7 @@ public class SubscriberServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_Stripe_CardPaymentMethod_Succeeds(
|
||||
public async Task GetPaymentSource_Stripe_CardPaymentMethod_Succeeds(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
@@ -491,43 +643,37 @@ public class SubscriberServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_Stripe_SetupIntentForBankAccount_Succeeds(
|
||||
public async Task GetPaymentSource_Stripe_SetupIntentForBankAccount_Succeeds(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = provider.GatewayCustomerId
|
||||
};
|
||||
var customer = new Customer { Id = provider.GatewayCustomerId };
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(
|
||||
options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")))
|
||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains(
|
||||
"invoice_settings.default_payment_method")))
|
||||
.Returns(customer);
|
||||
|
||||
var setupIntent = new SetupIntent
|
||||
{
|
||||
Id = "setup_intent_id",
|
||||
Status = "requires_action",
|
||||
NextAction = new SetupIntentNextAction
|
||||
{
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||
},
|
||||
NextAction =
|
||||
new SetupIntentNextAction
|
||||
{
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||
},
|
||||
PaymentMethod = new PaymentMethod
|
||||
{
|
||||
UsBankAccount = new PaymentMethodUsBankAccount
|
||||
{
|
||||
BankName = "Chase",
|
||||
Last4 = "9999"
|
||||
}
|
||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntent.Id);
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id, Arg.Is<SetupIntentGetOptions>(
|
||||
options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
|
||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id,
|
||||
Arg.Is<SetupIntentGetOptions>(options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
|
||||
@@ -537,24 +683,19 @@ public class SubscriberServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_Stripe_LegacyBankAccount_Succeeds(
|
||||
public async Task GetPaymentSource_Stripe_LegacyBankAccount_Succeeds(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
var customer = new Customer
|
||||
{
|
||||
DefaultSource = new BankAccount
|
||||
{
|
||||
Status = "verified",
|
||||
BankName = "Chase",
|
||||
Last4 = "9999"
|
||||
}
|
||||
DefaultSource = new BankAccount { Status = "verified", BankName = "Chase", Last4 = "9999" }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(
|
||||
options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")))
|
||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains(
|
||||
"invoice_settings.default_payment_method")))
|
||||
.Returns(customer);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
@@ -565,25 +706,19 @@ public class SubscriberServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_Stripe_LegacyCard_Succeeds(
|
||||
public async Task GetPaymentSource_Stripe_LegacyCard_Succeeds(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
var customer = new Customer
|
||||
{
|
||||
DefaultSource = new Card
|
||||
{
|
||||
Brand = "Visa",
|
||||
Last4 = "9999",
|
||||
ExpMonth = 9,
|
||||
ExpYear = 2028
|
||||
}
|
||||
DefaultSource = new Card { Brand = "Visa", Last4 = "9999", ExpMonth = 9, ExpYear = 2028 }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(
|
||||
options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")))
|
||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains(
|
||||
"invoice_settings.default_payment_method")))
|
||||
.Returns(customer);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
@@ -594,7 +729,7 @@ public class SubscriberServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_Stripe_LegacySourceCard_Succeeds(
|
||||
public async Task GetPaymentSource_Stripe_LegacySourceCard_Succeeds(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
@@ -1185,12 +1320,6 @@ public class SubscriberServiceTests
|
||||
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options => options.PaymentMethod == "TOKEN"))
|
||||
.Returns([matchingSetupIntent]);
|
||||
|
||||
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options => options.Customer == provider.GatewayCustomerId))
|
||||
.Returns([
|
||||
new SetupIntent { Id = "setup_intent_2", Status = "requires_payment_method" },
|
||||
new SetupIntent { Id = "setup_intent_3", Status = "succeeded" }
|
||||
]);
|
||||
|
||||
stripeAdapter.CustomerListPaymentMethods(provider.GatewayCustomerId).Returns([
|
||||
new PaymentMethod { Id = "payment_method_1" }
|
||||
]);
|
||||
@@ -1200,8 +1329,8 @@ public class SubscriberServiceTests
|
||||
|
||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_1");
|
||||
|
||||
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_2",
|
||||
Arg.Is<SetupIntentCancelOptions>(options => options.CancellationReason == "abandoned"));
|
||||
await stripeAdapter.DidNotReceive().SetupIntentCancel(Arg.Any<string>(),
|
||||
Arg.Any<SetupIntentCancelOptions>());
|
||||
|
||||
await stripeAdapter.Received(1).PaymentMethodDetachAsync("payment_method_1");
|
||||
|
||||
@@ -1229,12 +1358,6 @@ public class SubscriberServiceTests
|
||||
}
|
||||
});
|
||||
|
||||
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options => options.Customer == provider.GatewayCustomerId))
|
||||
.Returns([
|
||||
new SetupIntent { Id = "setup_intent_2", Status = "requires_payment_method" },
|
||||
new SetupIntent { Id = "setup_intent_3", Status = "succeeded" }
|
||||
]);
|
||||
|
||||
stripeAdapter.CustomerListPaymentMethods(provider.GatewayCustomerId).Returns([
|
||||
new PaymentMethod { Id = "payment_method_1" }
|
||||
]);
|
||||
@@ -1242,8 +1365,8 @@ public class SubscriberServiceTests
|
||||
await sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.Card, "TOKEN"));
|
||||
|
||||
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_2",
|
||||
Arg.Is<SetupIntentCancelOptions>(options => options.CancellationReason == "abandoned"));
|
||||
await stripeAdapter.DidNotReceive().SetupIntentCancel(Arg.Any<string>(),
|
||||
Arg.Any<SetupIntentCancelOptions>());
|
||||
|
||||
await stripeAdapter.Received(1).PaymentMethodDetachAsync("payment_method_1");
|
||||
|
||||
@@ -1741,7 +1864,7 @@ public class SubscriberServiceTests
|
||||
PaymentMethodId = "payment_method_id"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntent.Id);
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ public class PreviewTaxAmountCommandTests
|
||||
options.SubscriptionDetails.Items.Count == 1 &&
|
||||
options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
|
||||
options.SubscriptionDetails.Items[0].Quantity == 1 &&
|
||||
options.AutomaticTax.Enabled == false
|
||||
options.AutomaticTax.Enabled == true
|
||||
))
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
@@ -273,4 +273,269 @@ public class PreviewTaxAmountCommandTests
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support for assistance.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_USBased_PersonalUse_SetsAutomaticTaxEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.FamiliesAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
Assert.True(result.IsT0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_USBased_BusinessUse_SetsAutomaticTaxEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
Assert.True(result.IsT0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.FamiliesAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "CA",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
Assert.True(result.IsT0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "CA",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
Assert.True(result.IsT0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_USBased_PersonalUse_DoesNotSetTaxExempt()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.FamiliesAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
Assert.True(result.IsT0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_USBased_BusinessUse_DoesNotSetTaxExempt()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
Assert.True(result.IsT0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_NonUSBased_PersonalUse_DoesNotSetTaxExempt()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.FamiliesAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "CA",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_NonUSBased_BusinessUse_SetsTaxExemptReverse()
|
||||
{
|
||||
// Arrange
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
ProductType = ProductType.PasswordManager,
|
||||
TaxInformation = new TaxInformationDTO
|
||||
{
|
||||
Country = "CA",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(parameters.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
|
||||
|
||||
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
|
||||
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(expectedInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(parameters);
|
||||
|
||||
// Assert
|
||||
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse
|
||||
));
|
||||
Assert.True(result.IsT0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Tax.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AutomaticTaxFactoryTests
|
||||
{
|
||||
[BitAutoData]
|
||||
[Theory]
|
||||
public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenSubscriberIsUser(SutProvider<AutomaticTaxFactory> sut)
|
||||
{
|
||||
var parameters = new AutomaticTaxFactoryParameters(new User(), []);
|
||||
|
||||
var actual = await sut.Sut.CreateAsync(parameters);
|
||||
|
||||
Assert.IsType<PersonalUseAutomaticTaxStrategy>(actual);
|
||||
}
|
||||
|
||||
[BitAutoData]
|
||||
[Theory]
|
||||
public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenSubscriberIsOrganizationWithFamiliesAnnuallyPrice(
|
||||
SutProvider<AutomaticTaxFactory> sut)
|
||||
{
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
var parameters = new AutomaticTaxFactoryParameters(new Organization(), [familiesPlan.PasswordManager.StripePlanId]);
|
||||
|
||||
sut.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(new FamiliesPlan());
|
||||
|
||||
sut.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually2019))
|
||||
.Returns(new Families2019Plan());
|
||||
|
||||
var actual = await sut.Sut.CreateAsync(parameters);
|
||||
|
||||
Assert.IsType<PersonalUseAutomaticTaxStrategy>(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateAsync_ReturnsBusinessUseStrategy_WhenSubscriberIsOrganizationWithBusinessUsePrice(
|
||||
EnterpriseAnnually plan,
|
||||
SutProvider<AutomaticTaxFactory> sut)
|
||||
{
|
||||
var parameters = new AutomaticTaxFactoryParameters(new Organization(), [plan.PasswordManager.StripePlanId]);
|
||||
|
||||
sut.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(new FamiliesPlan());
|
||||
|
||||
sut.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually2019))
|
||||
.Returns(new Families2019Plan());
|
||||
|
||||
var actual = await sut.Sut.CreateAsync(parameters);
|
||||
|
||||
Assert.IsType<BusinessUseAutomaticTaxStrategy>(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenPlanIsMeantForPersonalUse(SutProvider<AutomaticTaxFactory> sut)
|
||||
{
|
||||
var parameters = new AutomaticTaxFactoryParameters(PlanType.FamiliesAnnually);
|
||||
sut.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == parameters.PlanType.Value))
|
||||
.Returns(new FamiliesPlan());
|
||||
|
||||
var actual = await sut.Sut.CreateAsync(parameters);
|
||||
|
||||
Assert.IsType<PersonalUseAutomaticTaxStrategy>(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CreateAsync_ReturnsBusinessUseStrategy_WhenPlanIsMeantForBusinessUse(SutProvider<AutomaticTaxFactory> sut)
|
||||
{
|
||||
var parameters = new AutomaticTaxFactoryParameters(PlanType.EnterpriseAnnually);
|
||||
sut.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == parameters.PlanType.Value))
|
||||
.Returns(new EnterprisePlan(true));
|
||||
|
||||
var actual = await sut.Sut.CreateAsync(parameters);
|
||||
|
||||
Assert.IsType<BusinessUseAutomaticTaxStrategy>(actual);
|
||||
}
|
||||
|
||||
public record EnterpriseAnnually : EnterprisePlan
|
||||
{
|
||||
public EnterpriseAnnually() : base(true)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,492 +0,0 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Tax.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class BusinessUseAutomaticTaxStrategyTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetUpdateOptions_ReturnsNull_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription();
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(false);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetUpdateOptions_ReturnsNull_WhenSubscriptionDoesNotNeedUpdating(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "US",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.False(actual.AutomaticTax.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForAmericanCustomers(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "US",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.True(actual.AutomaticTax.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "ES",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
},
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = new List<TaxId>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Country = "ES",
|
||||
Type = "eu_vat",
|
||||
Value = "ESZ8880999Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.True(actual.AutomaticTax.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetUpdateOptions_ThrowsArgumentNullException_WhenTaxIdsIsNull(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "ES",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
},
|
||||
TaxIds = null
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => sutProvider.Sut.GetUpdateOptions(subscription));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "ES",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
},
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = new List<TaxId>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.False(actual.AutomaticTax.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SetUpdateOptions_SetsNothing_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var options = new SubscriptionUpdateOptions();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new()
|
||||
{
|
||||
Country = "US"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.Sut.SetUpdateOptions(options, subscription);
|
||||
|
||||
Assert.Null(options.AutomaticTax);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SetUpdateOptions_SetsNothing_WhenSubscriptionDoesNotNeedUpdating(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var options = new SubscriptionUpdateOptions();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "US",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.Sut.SetUpdateOptions(options, subscription);
|
||||
|
||||
Assert.Null(options.AutomaticTax);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var options = new SubscriptionUpdateOptions();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.Sut.SetUpdateOptions(options, subscription);
|
||||
|
||||
Assert.False(options.AutomaticTax.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForAmericanCustomers(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var options = new SubscriptionUpdateOptions();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "US",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.Sut.SetUpdateOptions(options, subscription);
|
||||
|
||||
Assert.True(options.AutomaticTax!.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var options = new SubscriptionUpdateOptions();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "ES",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
},
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = new List<TaxId>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Country = "ES",
|
||||
Type = "eu_vat",
|
||||
Value = "ESZ8880999Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.Sut.SetUpdateOptions(options, subscription);
|
||||
|
||||
Assert.True(options.AutomaticTax!.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SetUpdateOptions_ThrowsArgumentNullException_WhenTaxIdsIsNull(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var options = new SubscriptionUpdateOptions();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "ES",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
},
|
||||
TaxIds = null
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => sutProvider.Sut.SetUpdateOptions(options, subscription));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds(
|
||||
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var options = new SubscriptionUpdateOptions();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "ES",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
},
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = new List<TaxId>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.Sut.SetUpdateOptions(options, subscription);
|
||||
|
||||
Assert.False(options.AutomaticTax!.Enabled);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Tax.Services;
|
||||
|
||||
/// <param name="isAutomaticTaxEnabled">
|
||||
/// Whether the subscription options will have automatic tax enabled or not.
|
||||
/// </param>
|
||||
public class FakeAutomaticTaxStrategy(
|
||||
bool isAutomaticTaxEnabled) : IAutomaticTaxStrategy
|
||||
{
|
||||
public SubscriptionUpdateOptions? GetUpdateOptions(Subscription subscription)
|
||||
{
|
||||
return new SubscriptionUpdateOptions
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled }
|
||||
};
|
||||
}
|
||||
|
||||
public void SetCreateOptions(SubscriptionCreateOptions options, Customer customer)
|
||||
{
|
||||
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled };
|
||||
}
|
||||
|
||||
public void SetUpdateOptions(SubscriptionUpdateOptions options, Subscription subscription)
|
||||
{
|
||||
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled };
|
||||
}
|
||||
|
||||
public void SetInvoiceCreatePreviewOptions(InvoiceCreatePreviewOptions options)
|
||||
{
|
||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled };
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Tax.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PersonalUseAutomaticTaxStrategyTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetUpdateOptions_ReturnsNull_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled(
|
||||
SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription();
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(false);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetUpdateOptions_ReturnsNull_WhenSubscriptionDoesNotNeedUpdating(
|
||||
SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "US",
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid(
|
||||
SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.False(actual.AutomaticTax.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("CA")]
|
||||
[BitAutoData("ES")]
|
||||
[BitAutoData("US")]
|
||||
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForAllCountries(
|
||||
string country, SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = country
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.True(actual.AutomaticTax.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("CA")]
|
||||
[BitAutoData("ES")]
|
||||
[BitAutoData("US")]
|
||||
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds(
|
||||
string country, SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = country,
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
},
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = new List<TaxId>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Country = "ES",
|
||||
Type = "eu_vat",
|
||||
Value = "ESZ8880999Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.True(actual.AutomaticTax.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("CA")]
|
||||
[BitAutoData("ES")]
|
||||
[BitAutoData("US")]
|
||||
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds(
|
||||
string country, SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
|
||||
{
|
||||
var subscription = new Subscription
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTax
|
||||
{
|
||||
Enabled = false
|
||||
},
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = country
|
||||
},
|
||||
Tax = new CustomerTax
|
||||
{
|
||||
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
|
||||
},
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = new List<TaxId>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
|
||||
.Returns(true);
|
||||
|
||||
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.True(actual.AutomaticTax.Enabled);
|
||||
}
|
||||
}
|
||||
733
test/Core.Test/Context/CurrentContextTests.cs
Normal file
733
test/Core.Test/Context/CurrentContextTests.cs
Normal file
@@ -0,0 +1,733 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Context;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Context;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class CurrentContextTests
|
||||
{
|
||||
#region BuildAsync(HttpContext) Tests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildAsync_HttpContext_SetsHttpContext(
|
||||
SutProvider<CurrentContext> sutProvider)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var globalSettings = new Core.Settings.GlobalSettings();
|
||||
// Act
|
||||
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(httpContext, sutProvider.Sut.HttpContext);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildAsync_HttpContext_OnlyBuildsOnce(
|
||||
SutProvider<CurrentContext> sutProvider)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var globalSettings = new Core.Settings.GlobalSettings();
|
||||
// Arrange
|
||||
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
|
||||
var firstContext = sutProvider.Sut.HttpContext;
|
||||
|
||||
var secondHttpContext = new DefaultHttpContext();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.BuildAsync(secondHttpContext, globalSettings);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(firstContext, sutProvider.Sut.HttpContext);
|
||||
Assert.NotEqual(secondHttpContext, sutProvider.Sut.HttpContext);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildAsync_HttpContext_SetsDeviceIdentifier(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
string expectedValue)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var globalSettings = new Core.Settings.GlobalSettings();
|
||||
sutProvider.Sut.DeviceIdentifier = null;
|
||||
// Arrange
|
||||
httpContext.Request.Headers["Device-Identifier"] = expectedValue;
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedValue, sutProvider.Sut.DeviceIdentifier);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildAsync_HttpContext_SetsCountryName(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
string countryName)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var globalSettings = new Core.Settings.GlobalSettings();
|
||||
// Arrange
|
||||
httpContext.Request.Headers["country-name"] = countryName;
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(countryName, sutProvider.Sut.CountryName);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildAsync_HttpContext_SetsDeviceType(
|
||||
SutProvider<CurrentContext> sutProvider)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var globalSettings = new Core.Settings.GlobalSettings();
|
||||
// Arrange
|
||||
var deviceType = DeviceType.Android;
|
||||
httpContext.Request.Headers["Device-Type"] = ((int)deviceType).ToString();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(deviceType, sutProvider.Sut.DeviceType);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildAsync_HttpContext_SetsCloudflareFlags(
|
||||
SutProvider<CurrentContext> sutProvider)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var globalSettings = new Core.Settings.GlobalSettings();
|
||||
sutProvider.Sut.BotScore = null;
|
||||
// Arrange
|
||||
var botScore = 85;
|
||||
httpContext.Request.Headers["X-Cf-Bot-Score"] = botScore.ToString();
|
||||
httpContext.Request.Headers["X-Cf-Worked-Proxied"] = "1";
|
||||
httpContext.Request.Headers["X-Cf-Is-Bot"] = "1";
|
||||
httpContext.Request.Headers["X-Cf-Maybe-Bot"] = "1";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
|
||||
|
||||
// Assert
|
||||
Assert.True(sutProvider.Sut.CloudflareWorkerProxied);
|
||||
Assert.True(sutProvider.Sut.IsBot);
|
||||
Assert.True(sutProvider.Sut.MaybeBot);
|
||||
Assert.Equal(botScore, sutProvider.Sut.BotScore);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildAsync_HttpContext_SetsClientVersion(
|
||||
SutProvider<CurrentContext> sutProvider)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var globalSettings = new Core.Settings.GlobalSettings();
|
||||
// Arrange
|
||||
var version = "2024.1.0";
|
||||
httpContext.Request.Headers["Bitwarden-Client-Version"] = version;
|
||||
httpContext.Request.Headers["Is-Prerelease"] = "1";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new Version(version), sutProvider.Sut.ClientVersion);
|
||||
Assert.True(sutProvider.Sut.ClientVersionIsPrerelease);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SetContextAsync Tests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_NullUser_DoesNotThrow(
|
||||
SutProvider<CurrentContext> sutProvider)
|
||||
{
|
||||
// Act & Assert
|
||||
await sutProvider.Sut.SetContextAsync(null);
|
||||
// Should not throw
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_UserWithNoClaims_DoesNotThrow(
|
||||
SutProvider<CurrentContext> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var user = new ClaimsPrincipal();
|
||||
|
||||
// Act & Assert
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
// Should not throw
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_SendClient_ShortCircuits(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.UserId = null;
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(Claims.Type, IdentityClientType.Send.ToString()),
|
||||
new("sub", userId.ToString())
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(IdentityClientType.Send, sutProvider.Sut.IdentityClientType);
|
||||
Assert.Null(sutProvider.Sut.UserId); // Should not be set for Send clients
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_RegularUser_SetsUserId(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid userId,
|
||||
string clientId)
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new("sub", userId.ToString()),
|
||||
new("client_id", clientId)
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(userId, sutProvider.Sut.UserId);
|
||||
Assert.Equal(clientId, sutProvider.Sut.ClientId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_InstallationClient_SetsInstallationId(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid installationId)
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new("client_id", "installation.12345"),
|
||||
new("client_sub", installationId.ToString())
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(installationId, sutProvider.Sut.InstallationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_OrganizationClient_SetsOrganizationId(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new("client_id", "organization.12345"),
|
||||
new("client_sub", organizationId.ToString())
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(organizationId, sutProvider.Sut.OrganizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_ServiceAccount_SetsServiceAccountOrganizationId(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(Claims.Type, IdentityClientType.ServiceAccount.ToString()),
|
||||
new(Claims.Organization, organizationId.ToString())
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(IdentityClientType.ServiceAccount, sutProvider.Sut.IdentityClientType);
|
||||
Assert.Equal(organizationId, sutProvider.Sut.ServiceAccountOrganizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_WithDeviceClaims_SetsDeviceInfo(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
string deviceIdentifier)
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(Claims.Device, deviceIdentifier),
|
||||
new(Claims.DeviceType, ((int)DeviceType.iOS).ToString())
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(deviceIdentifier, sutProvider.Sut.DeviceIdentifier);
|
||||
Assert.Equal(DeviceType.iOS, sutProvider.Sut.DeviceType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Organization Claims Tests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(Claims.OrganizationOwner, OrganizationUserType.Owner)]
|
||||
[BitAutoData(Claims.OrganizationAdmin, OrganizationUserType.Admin)]
|
||||
[BitAutoData(Claims.OrganizationUser, OrganizationUserType.User)]
|
||||
public async Task SetContextAsync_OrganizationClaims_SetsOrganizations(
|
||||
string userOrgAssociation,
|
||||
OrganizationUserType userType,
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid org1Id,
|
||||
Guid org2Id)
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(userOrgAssociation, org1Id.ToString()),
|
||||
new(userOrgAssociation, org2Id.ToString()),
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, sutProvider.Sut.Organizations.Count);
|
||||
Assert.All(sutProvider.Sut.Organizations, org => Assert.Equal(userType, org.Type));
|
||||
Assert.Contains(sutProvider.Sut.Organizations, org => org.Id == org1Id);
|
||||
Assert.Contains(sutProvider.Sut.Organizations, org => org.Id == org2Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_OrganizationCustomClaims_SetsOrganizationsWithPermissions(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(Claims.OrganizationCustom, orgId.ToString()),
|
||||
new("accesseventlogs", orgId.ToString()),
|
||||
new("manageusers", orgId.ToString())
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Single(sutProvider.Sut.Organizations);
|
||||
var org = sutProvider.Sut.Organizations.First();
|
||||
Assert.Equal(OrganizationUserType.Custom, org.Type);
|
||||
Assert.Equal(orgId, org.Id);
|
||||
Assert.True(org.Permissions.AccessEventLogs);
|
||||
Assert.True(org.Permissions.ManageUsers);
|
||||
Assert.False(org.Permissions.ManageGroups);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_SecretsManagerAccess_SetsAccessSecretsManager(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(Claims.OrganizationOwner, orgId.ToString()),
|
||||
new(Claims.SecretsManagerAccess, orgId.ToString())
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Single(sutProvider.Sut.Organizations);
|
||||
Assert.True(sutProvider.Sut.Organizations.First().AccessSecretsManager);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Provider Claims Tests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_ProviderAdminClaims_SetsProviders(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(Claims.ProviderAdmin, providerId.ToString())
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Single(sutProvider.Sut.Providers);
|
||||
Assert.Equal(ProviderUserType.ProviderAdmin, sutProvider.Sut.Providers.First().Type);
|
||||
Assert.Equal(providerId, sutProvider.Sut.Providers.First().Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetContextAsync_ProviderServiceUserClaims_SetsProviders(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(Claims.ProviderServiceUser, providerId.ToString())
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetContextAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.Single(sutProvider.Sut.Providers);
|
||||
Assert.Equal(ProviderUserType.ServiceUser, sutProvider.Sut.Providers.First().Type);
|
||||
Assert.Equal(providerId, sutProvider.Sut.Providers.First().Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Organization Permission Tests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OrganizationUser_WithDirectAccess_ReturnsTrue(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = orgId, Type = OrganizationUserType.User }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.OrganizationUser(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OrganizationUser_WithoutAccess_ReturnsFalse(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>();
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.OrganizationUser(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OrganizationAdmin_WithAdminAccess_ReturnsTrue(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = orgId, Type = OrganizationUserType.Admin }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.OrganizationAdmin(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OrganizationOwner_WithOwnerAccess_ReturnsTrue(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = orgId, Type = OrganizationUserType.Owner }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.OrganizationOwner(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OrganizationCustom_WithCustomAccess_ReturnsTrue(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = orgId, Type = OrganizationUserType.Custom }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.OrganizationCustom(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AccessEventLogs_WithPermission_ReturnsTrue(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = orgId,
|
||||
Type = OrganizationUserType.Custom,
|
||||
Permissions = new Permissions { AccessEventLogs = true }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AccessEventLogs(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Provider Permission Tests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ProviderProviderAdmin_WithAdminAccess_ReturnsTrue(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Providers = new List<CurrentContextProvider>
|
||||
{
|
||||
new() { Id = providerId, Type = ProviderUserType.ProviderAdmin }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.ProviderProviderAdmin(providerId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ProviderUser_WithAnyAccess_ReturnsTrue(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Providers = new List<CurrentContextProvider>
|
||||
{
|
||||
new() { Id = providerId, Type = ProviderUserType.ServiceUser }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.ProviderUser(providerId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Secrets Manager Tests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void AccessSecretsManager_WithServiceAccount_ReturnsTrue(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.ServiceAccountOrganizationId = orgId;
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.AccessSecretsManager(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void AccessSecretsManager_WithOrgAccess_ReturnsTrue(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = orgId, AccessSecretsManager = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.AccessSecretsManager(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void AccessSecretsManager_WithoutAccess_ReturnsFalse(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = orgId, AccessSecretsManager = false }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.AccessSecretsManager(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Membership Loading Tests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task OrganizationMembershipAsync_LoadsFromRepository(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid userId,
|
||||
List<OrganizationUserOrganizationDetails> userOrgs)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.UserId = userId;
|
||||
sutProvider.Sut.Organizations = null;
|
||||
var organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
userOrgs.ForEach(org => org.Status = OrganizationUserStatusType.Confirmed);
|
||||
|
||||
// Test complains about the JSON object that we store permissions as, so just set to empty object to pass the test.
|
||||
userOrgs.ForEach(org => org.Permissions = "{}");
|
||||
organizationUserRepository.GetManyDetailsByUserAsync(userId)
|
||||
.Returns(userOrgs);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.OrganizationMembershipAsync(organizationUserRepository, userId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(userOrgs.Count, result.Count);
|
||||
Assert.Equal(userId, sutProvider.Sut.UserId);
|
||||
await organizationUserRepository.Received(1).GetManyDetailsByUserAsync(userId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ProviderMembershipAsync_LoadsFromRepository(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid userId,
|
||||
List<ProviderUser> userProviders)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.UserId = userId;
|
||||
sutProvider.Sut.Providers = null;
|
||||
|
||||
var providerUserRepository = Substitute.For<IProviderUserRepository>();
|
||||
userProviders.ForEach(provider => provider.Status = ProviderUserStatusType.Confirmed);
|
||||
|
||||
// Test complains about the JSON object that we store permissions as, so just set to empty object to pass the test.
|
||||
userProviders.ForEach(provider => provider.Permissions = "{}");
|
||||
providerUserRepository.GetManyByUserAsync(userId)
|
||||
.Returns(userProviders);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ProviderMembershipAsync(providerUserRepository, userId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(userProviders.Count, result.Count);
|
||||
Assert.Equal(userId, sutProvider.Sut.UserId);
|
||||
await providerUserRepository.Received(1).GetManyByUserAsync(userId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility Tests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void GetOrganization_WithExistingOrg_ReturnsOrganization(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
var org = new CurrentContextOrganization { Id = orgId };
|
||||
sutProvider.Sut.Organizations = new List<CurrentContextOrganization> { org };
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.GetOrganization(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(org, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void GetOrganization_WithNonExistingOrg_ReturnsNull(
|
||||
SutProvider<CurrentContext> sutProvider,
|
||||
Guid orgId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.Sut.Organizations = new List<CurrentContextOrganization>();
|
||||
|
||||
// Act
|
||||
var result = sutProvider.Sut.GetOrganization(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DeleteOrganizationReportCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task DropOrganizationReportAsync_withValidRequest_Success(
|
||||
SutProvider<DropOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var OrganizationReports = fixture.CreateMany<OrganizationReport>(2).ToList();
|
||||
// only take one id from the list - we only want to drop one record
|
||||
var request = fixture.Build<DropOrganizationReportRequest>()
|
||||
.With(x => x.OrganizationReportIds,
|
||||
OrganizationReports.Select(x => x.Id).Take(1).ToList())
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByOrganizationIdAsync(Arg.Any<Guid>())
|
||||
.Returns(OrganizationReports);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DropOrganizationReportAsync(request);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1)
|
||||
.GetByOrganizationIdAsync(request.OrganizationId);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1)
|
||||
.DeleteAsync(Arg.Is<OrganizationReport>(_ =>
|
||||
request.OrganizationReportIds.Contains(_.Id)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DropOrganizationReportAsync_withValidRequest_nothingToDrop(
|
||||
SutProvider<DropOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var OrganizationReports = fixture.CreateMany<OrganizationReport>(2).ToList();
|
||||
// we are passing invalid data
|
||||
var request = fixture.Build<DropOrganizationReportRequest>()
|
||||
.With(x => x.OrganizationReportIds, new List<Guid> { Guid.NewGuid() })
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByOrganizationIdAsync(Arg.Any<Guid>())
|
||||
.Returns(OrganizationReports);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DropOrganizationReportAsync(request);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1)
|
||||
.GetByOrganizationIdAsync(request.OrganizationId);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(0)
|
||||
.DeleteAsync(Arg.Any<OrganizationReport>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DropOrganizationReportAsync_withNodata_fails(
|
||||
SutProvider<DropOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
// we are passing invalid data
|
||||
var request = fixture.Build<DropOrganizationReportRequest>()
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByOrganizationIdAsync(Arg.Any<Guid>())
|
||||
.Returns(null as List<OrganizationReport>);
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.DropOrganizationReportAsync(request));
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1)
|
||||
.GetByOrganizationIdAsync(request.OrganizationId);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(0)
|
||||
.DeleteAsync(Arg.Any<OrganizationReport>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DropOrganizationReportAsync_withInvalidOrganizationId_ShouldThrowError(
|
||||
SutProvider<DropOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<DropOrganizationReportRequest>();
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByOrganizationIdAsync(Arg.Any<Guid>())
|
||||
.Returns(null as List<OrganizationReport>);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
|
||||
Assert.Equal("No data found.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DropOrganizationReportAsync_withInvalidOrganizationReportId_ShouldThrowError(
|
||||
SutProvider<DropOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<DropOrganizationReportRequest>();
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByOrganizationIdAsync(Arg.Any<Guid>())
|
||||
.Returns(new List<OrganizationReport>());
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
|
||||
Assert.Equal("No data found.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DropOrganizationReportAsync_withNullOrganizationId_ShouldThrowError(
|
||||
SutProvider<DropOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<DropOrganizationReportRequest>()
|
||||
.With(x => x.OrganizationId, default(Guid))
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
|
||||
Assert.Equal("No data found.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DropOrganizationReportAsync_withNullOrganizationReportIds_ShouldThrowError(
|
||||
SutProvider<DropOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<DropOrganizationReportRequest>()
|
||||
.With(x => x.OrganizationReportIds, default(List<Guid>))
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
|
||||
Assert.Equal("No data found.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DropOrganizationReportAsync_withEmptyOrganizationReportIds_ShouldThrowError(
|
||||
SutProvider<DropOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<DropOrganizationReportRequest>()
|
||||
.With(x => x.OrganizationReportIds, new List<Guid>())
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
|
||||
Assert.Equal("No data found.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DropOrganizationReportAsync_withEmptyRequest_ShouldThrowError(
|
||||
SutProvider<DropOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var request = new DropOrganizationReportRequest();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.DropOrganizationReportAsync(request));
|
||||
Assert.Equal("No data found.", exception.Message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Dirt.Models.Data;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetOrganizationReportApplicationDataQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportApplicationDataAsync_WithValidParams_ShouldReturnApplicationData(
|
||||
SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = fixture.Create<Guid>();
|
||||
var reportId = fixture.Create<Guid>();
|
||||
var applicationDataResponse = fixture.Build<OrganizationReportApplicationDataResponse>()
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetApplicationDataAsync(reportId)
|
||||
.Returns(applicationDataResponse);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1).GetApplicationDataAsync(reportId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportApplicationDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var reportId = Guid.NewGuid();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(Guid.Empty, reportId));
|
||||
|
||||
Assert.Equal("OrganizationId is required.", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().GetApplicationDataAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportApplicationDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(
|
||||
SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, Guid.Empty));
|
||||
|
||||
Assert.Equal("ReportId is required.", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().GetApplicationDataAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportApplicationDataAsync_WhenDataNotFound_ShouldThrowNotFoundException(
|
||||
SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var reportId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetApplicationDataAsync(reportId)
|
||||
.Returns((OrganizationReportApplicationDataResponse)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId));
|
||||
|
||||
Assert.Equal("Organization report application data not found.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportApplicationDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(
|
||||
SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var reportId = Guid.NewGuid();
|
||||
var expectedMessage = "Database connection failed";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetApplicationDataAsync(reportId)
|
||||
.Throws(new InvalidOperationException(expectedMessage));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId));
|
||||
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Dirt.Models.Data;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetOrganizationReportDataQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportDataAsync_WithValidParams_ShouldReturnReportData(
|
||||
SutProvider<GetOrganizationReportDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = fixture.Create<Guid>();
|
||||
var reportId = fixture.Create<Guid>();
|
||||
var reportDataResponse = fixture.Build<OrganizationReportDataResponse>()
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetReportDataAsync(reportId)
|
||||
.Returns(reportDataResponse);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1).GetReportDataAsync(reportId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<GetOrganizationReportDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var reportId = Guid.NewGuid();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportDataAsync(Guid.Empty, reportId));
|
||||
|
||||
Assert.Equal("OrganizationId is required.", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().GetReportDataAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(
|
||||
SutProvider<GetOrganizationReportDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, Guid.Empty));
|
||||
|
||||
Assert.Equal("ReportId is required.", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().GetReportDataAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportDataAsync_WhenDataNotFound_ShouldThrowNotFoundException(
|
||||
SutProvider<GetOrganizationReportDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var reportId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetReportDataAsync(reportId)
|
||||
.Returns((OrganizationReportDataResponse)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId));
|
||||
|
||||
Assert.Equal("Organization report data not found.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(
|
||||
SutProvider<GetOrganizationReportDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var reportId = Guid.NewGuid();
|
||||
var expectedMessage = "Database connection failed";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetReportDataAsync(reportId)
|
||||
.Throws(new InvalidOperationException(expectedMessage));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId));
|
||||
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetOrganizationReportQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportAsync_WithValidOrganizationId_ShouldReturnOrganizationReport(
|
||||
SutProvider<GetOrganizationReportQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = fixture.Create<Guid>();
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByOrganizationIdAsync(Arg.Any<Guid>())
|
||||
.Returns(fixture.CreateMany<OrganizationReport>(2).ToList());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationReportAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Count() == 2);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportAsync_WithInvalidOrganizationId_ShouldFail(
|
||||
SutProvider<GetOrganizationReportQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByOrganizationIdAsync(Arg.Is<Guid>(x => x == Guid.Empty))
|
||||
.Returns(new List<OrganizationReport>());
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(Guid.Empty));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("OrganizationId is required.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetLatestOrganizationReportAsync_WithValidOrganizationId_ShouldReturnOrganizationReport(
|
||||
SutProvider<GetOrganizationReportQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = fixture.Create<Guid>();
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetLatestByOrganizationIdAsync(Arg.Any<Guid>())
|
||||
.Returns(fixture.Create<OrganizationReport>());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetLatestOrganizationReportAsync_WithInvalidOrganizationId_ShouldFail(
|
||||
SutProvider<GetOrganizationReportQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetLatestByOrganizationIdAsync(Arg.Is<Guid>(x => x == Guid.Empty))
|
||||
.Returns(default(OrganizationReport));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(Guid.Empty));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("OrganizationId is required.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportAsync_WithNoReports_ShouldReturnEmptyList(
|
||||
SutProvider<GetOrganizationReportQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = fixture.Create<Guid>();
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByOrganizationIdAsync(Arg.Any<Guid>())
|
||||
.Returns(new List<OrganizationReport>());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationReportAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetLatestOrganizationReportAsync_WithNoReports_ShouldReturnNull(
|
||||
SutProvider<GetOrganizationReportQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = fixture.Create<Guid>();
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetLatestByOrganizationIdAsync(Arg.Any<Guid>())
|
||||
.Returns(default(OrganizationReport));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportAsync_WithNullOrganizationId_ShouldThrowException(
|
||||
SutProvider<GetOrganizationReportQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = default(Guid);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(organizationId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("OrganizationId is required.", exception.Message);
|
||||
}
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetLatestOrganizationReportAsync_WithNullOrganizationId_ShouldThrowException(
|
||||
SutProvider<GetOrganizationReportQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = default(Guid);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("OrganizationId is required.", exception.Message);
|
||||
}
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowException(
|
||||
SutProvider<GetOrganizationReportQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = Guid.Empty;
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetOrganizationReportAsync(organizationId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("OrganizationId is required.", exception.Message);
|
||||
}
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetLatestOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowException(
|
||||
SutProvider<GetOrganizationReportQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = Guid.Empty;
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("OrganizationId is required.", exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Dirt.Models.Data;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetOrganizationReportSummaryDataByDateRangeQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithValidParams_ShouldReturnSummaryData(
|
||||
SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = fixture.Create<Guid>();
|
||||
var reportId = fixture.Create<Guid>();
|
||||
var startDate = DateTime.UtcNow.AddDays(-30);
|
||||
var endDate = DateTime.UtcNow;
|
||||
var summaryDataList = fixture.Build<OrganizationReportSummaryDataResponse>()
|
||||
.CreateMany(3);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate)
|
||||
.Returns(summaryDataList);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(3, result.Count());
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1).GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var reportId = Guid.NewGuid();
|
||||
var startDate = DateTime.UtcNow.AddDays(-30);
|
||||
var endDate = DateTime.UtcNow;
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(Guid.Empty, startDate, endDate));
|
||||
|
||||
Assert.Equal("OrganizationId is required", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive()
|
||||
.GetSummaryDataByDateRangeAsync(
|
||||
Arg.Any<Guid>(),
|
||||
Arg.Any<DateTime>(),
|
||||
Arg.Any<DateTime>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithStartDateAfterEndDate_ShouldThrowBadRequestException(
|
||||
SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var reportId = Guid.NewGuid();
|
||||
var startDate = DateTime.UtcNow;
|
||||
var endDate = DateTime.UtcNow.AddDays(-30);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate));
|
||||
|
||||
Assert.Equal("StartDate must be earlier than or equal to EndDate", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().GetSummaryDataByDateRangeAsync(Arg.Any<Guid>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithEmptyResult_ShouldReturnEmptyList(
|
||||
SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var reportId = Guid.NewGuid();
|
||||
var startDate = DateTime.UtcNow.AddDays(-30);
|
||||
var endDate = DateTime.UtcNow;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate)
|
||||
.Returns(new List<OrganizationReportSummaryDataResponse>());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WhenRepositoryThrowsException_ShouldPropagateException(
|
||||
SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var reportId = Guid.NewGuid();
|
||||
var startDate = DateTime.UtcNow.AddDays(-30);
|
||||
var endDate = DateTime.UtcNow;
|
||||
var expectedMessage = "Database connection failed";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate)
|
||||
.Throws(new InvalidOperationException(expectedMessage));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate));
|
||||
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Dirt.Models.Data;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetOrganizationReportSummaryDataQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportSummaryDataAsync_WithValidParams_ShouldReturnSummaryData(
|
||||
SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = fixture.Create<Guid>();
|
||||
var reportId = fixture.Create<Guid>();
|
||||
var summaryDataResponse = fixture.Build<OrganizationReportSummaryDataResponse>()
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetSummaryDataAsync(reportId)
|
||||
.Returns(summaryDataResponse);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1).GetSummaryDataAsync(reportId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportSummaryDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var reportId = Guid.NewGuid();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(Guid.Empty, reportId));
|
||||
|
||||
Assert.Equal("OrganizationId is required.", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().GetSummaryDataAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportSummaryDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(
|
||||
SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, Guid.Empty));
|
||||
|
||||
Assert.Equal("ReportId is required.", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().GetSummaryDataAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportSummaryDataAsync_WhenDataNotFound_ShouldThrowNotFoundException(
|
||||
SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var reportId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetSummaryDataAsync(reportId)
|
||||
.Returns((OrganizationReportSummaryDataResponse)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId));
|
||||
|
||||
Assert.Equal("Organization report summary data not found.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetOrganizationReportSummaryDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(
|
||||
SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var reportId = Guid.NewGuid();
|
||||
var expectedMessage = "Database connection failed";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetSummaryDataAsync(reportId)
|
||||
.Throws(new InvalidOperationException(expectedMessage));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId));
|
||||
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class UpdateOrganizationReportApplicationDataCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportApplicationDataAsync_WithValidRequest_ShouldReturnUpdatedReport(
|
||||
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()
|
||||
.With(x => x.Id, Guid.NewGuid())
|
||||
.With(x => x.OrganizationId, Guid.NewGuid())
|
||||
.With(x => x.ApplicationData, "updated application data")
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.Id)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.Create();
|
||||
var updatedReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.Id)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.Id)
|
||||
.Returns(existingReport);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData)
|
||||
.Returns(updatedReport);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(updatedReport.Id, result.Id);
|
||||
Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1).UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()
|
||||
.With(x => x.OrganizationId, Guid.Empty)
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
|
||||
|
||||
Assert.Equal("OrganizationId is required", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().UpdateApplicationDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()
|
||||
.With(x => x.Id, Guid.Empty)
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
|
||||
|
||||
Assert.Equal("Id is required", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().UpdateApplicationDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportApplicationDataAsync_WithInvalidOrganization_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
|
||||
|
||||
Assert.Equal("Invalid Organization", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyApplicationData_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()
|
||||
.With(x => x.ApplicationData, string.Empty)
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
|
||||
|
||||
Assert.Equal("Application Data is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportApplicationDataAsync_WithNullApplicationData_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()
|
||||
.With(x => x.ApplicationData, (string)null)
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
|
||||
|
||||
Assert.Equal("Application Data is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportApplicationDataAsync_WithNonExistentReport_ShouldThrowNotFoundException(
|
||||
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.Id)
|
||||
.Returns((OrganizationReport)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
|
||||
|
||||
Assert.Equal("Organization report not found", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportApplicationDataAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.Id)
|
||||
.With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.Id)
|
||||
.Returns(existingReport);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
|
||||
|
||||
Assert.Equal("Organization report does not belong to the specified organization", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportApplicationDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(
|
||||
SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.Id)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.Id)
|
||||
.Returns(existingReport);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData)
|
||||
.Throws(new InvalidOperationException("Database connection failed"));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));
|
||||
|
||||
Assert.Equal("Database connection failed", exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
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.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class UpdateOrganizationReportCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportAsync_WithValidRequest_ShouldReturnUpdatedReport(
|
||||
SutProvider<UpdateOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportRequest>()
|
||||
.With(x => x.ReportId, Guid.NewGuid())
|
||||
.With(x => x.OrganizationId, Guid.NewGuid())
|
||||
.With(x => x.ReportData, "updated report data")
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.Create();
|
||||
var updatedReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.With(x => x.ReportData, request.ReportData)
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns(existingReport);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.UpsertAsync(Arg.Any<OrganizationReport>())
|
||||
.Returns(Task.CompletedTask);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns(existingReport, updatedReport);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateOrganizationReportAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(updatedReport.Id, result.Id);
|
||||
Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);
|
||||
Assert.Equal(updatedReport.ReportData, result.ReportData);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.Received(1).GetByIdAsync(request.OrganizationId);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(2).GetByIdAsync(request.ReportId);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1).UpsertAsync(Arg.Any<OrganizationReport>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportRequest>()
|
||||
.With(x => x.OrganizationId, Guid.Empty)
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
|
||||
|
||||
Assert.Equal("OrganizationId is required", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().UpsertAsync(Arg.Any<OrganizationReport>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportAsync_WithEmptyReportId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportRequest>()
|
||||
.With(x => x.ReportId, Guid.Empty)
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
|
||||
|
||||
Assert.Equal("ReportId is required", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().UpsertAsync(Arg.Any<OrganizationReport>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportAsync_WithInvalidOrganization_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportRequest>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
|
||||
|
||||
Assert.Equal("Invalid Organization", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportAsync_WithEmptyReportData_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportRequest>()
|
||||
.With(x => x.ReportData, string.Empty)
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
|
||||
|
||||
Assert.Equal("Report Data is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportAsync_WithNullReportData_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportRequest>()
|
||||
.With(x => x.ReportData, (string)null)
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
|
||||
|
||||
Assert.Equal("Report Data is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportAsync_WithNonExistentReport_ShouldThrowNotFoundException(
|
||||
SutProvider<UpdateOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns((OrganizationReport)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
|
||||
|
||||
Assert.Equal("Organization report not found", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns(existingReport);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportAsync(request));
|
||||
|
||||
Assert.Equal("Organization report does not belong to the specified organization", exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class UpdateOrganizationReportDataCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportDataAsync_WithValidRequest_ShouldReturnUpdatedReport(
|
||||
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportDataRequest>()
|
||||
.With(x => x.ReportId, Guid.NewGuid())
|
||||
.With(x => x.OrganizationId, Guid.NewGuid())
|
||||
.With(x => x.ReportData, "updated report data")
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.Create();
|
||||
var updatedReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns(existingReport);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData)
|
||||
.Returns(updatedReport);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateOrganizationReportDataAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(updatedReport.Id, result.Id);
|
||||
Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1).UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportDataRequest>()
|
||||
.With(x => x.OrganizationId, Guid.Empty)
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
|
||||
|
||||
Assert.Equal("OrganizationId is required", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().UpdateReportDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportDataRequest>()
|
||||
.With(x => x.ReportId, Guid.Empty)
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
|
||||
|
||||
Assert.Equal("ReportId is required", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().UpdateReportDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportDataAsync_WithInvalidOrganization_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportDataRequest>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
|
||||
|
||||
Assert.Equal("Invalid Organization", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportDataAsync_WithEmptyReportData_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportDataRequest>()
|
||||
.With(x => x.ReportData, string.Empty)
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
|
||||
|
||||
Assert.Equal("Report Data is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportDataAsync_WithNullReportData_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportDataRequest>()
|
||||
.With(x => x.ReportData, (string)null)
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
|
||||
|
||||
Assert.Equal("Report Data is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportDataAsync_WithNonExistentReport_ShouldThrowNotFoundException(
|
||||
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportDataRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns((OrganizationReport)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
|
||||
|
||||
Assert.Equal("Organization report not found", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportDataAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportDataRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns(existingReport);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
|
||||
|
||||
Assert.Equal("Organization report does not belong to the specified organization", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(
|
||||
SutProvider<UpdateOrganizationReportDataCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportDataRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns(existingReport);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData)
|
||||
.Throws(new InvalidOperationException("Database connection failed"));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));
|
||||
|
||||
Assert.Equal("Database connection failed", exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class UpdateOrganizationReportSummaryCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportSummaryAsync_WithValidRequest_ShouldReturnUpdatedReport(
|
||||
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()
|
||||
.With(x => x.ReportId, Guid.NewGuid())
|
||||
.With(x => x.OrganizationId, Guid.NewGuid())
|
||||
.With(x => x.SummaryData, "updated summary data")
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.Create();
|
||||
var updatedReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns(existingReport);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData)
|
||||
.Returns(updatedReport);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(updatedReport.Id, result.Id);
|
||||
Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.Received(1).UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportSummaryAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()
|
||||
.With(x => x.OrganizationId, Guid.Empty)
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
|
||||
|
||||
Assert.Equal("OrganizationId is required", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().UpdateSummaryDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportSummaryAsync_WithEmptyReportId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()
|
||||
.With(x => x.ReportId, Guid.Empty)
|
||||
.Create();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
|
||||
|
||||
Assert.Equal("ReportId is required", exception.Message);
|
||||
await sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.DidNotReceive().UpdateSummaryDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportSummaryAsync_WithInvalidOrganization_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
|
||||
|
||||
Assert.Equal("Invalid Organization", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportSummaryAsync_WithEmptySummaryData_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()
|
||||
.With(x => x.SummaryData, string.Empty)
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
|
||||
|
||||
Assert.Equal("Summary Data is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportSummaryAsync_WithNullSummaryData_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()
|
||||
.With(x => x.SummaryData, (string)null)
|
||||
.Create();
|
||||
|
||||
var organization = fixture.Create<Organization>();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
|
||||
|
||||
Assert.Equal("Summary Data is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportSummaryAsync_WithNonExistentReport_ShouldThrowNotFoundException(
|
||||
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns((OrganizationReport)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
|
||||
|
||||
Assert.Equal("Organization report not found", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportSummaryAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(
|
||||
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns(existingReport);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
|
||||
|
||||
Assert.Equal("Organization report does not belong to the specified organization", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateOrganizationReportSummaryAsync_WhenRepositoryThrowsException_ShouldPropagateException(
|
||||
SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();
|
||||
var organization = fixture.Create<Organization>();
|
||||
var existingReport = fixture.Build<OrganizationReport>()
|
||||
.With(x => x.Id, request.ReportId)
|
||||
.With(x => x.OrganizationId, request.OrganizationId)
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(request.OrganizationId)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.GetByIdAsync(request.ReportId)
|
||||
.Returns(existingReport);
|
||||
sutProvider.GetDependency<IOrganizationReportRepository>()
|
||||
.UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData)
|
||||
.Throws(new InvalidOperationException("Database connection failed"));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));
|
||||
|
||||
Assert.Equal("Database connection failed", exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -22,18 +22,18 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Push.Services;
|
||||
namespace Bit.Core.Test.Platform.Push.Engines;
|
||||
|
||||
[QueueClientCustomize]
|
||||
[SutProviderCustomize]
|
||||
public class AzureQueuePushNotificationServiceTests
|
||||
public class AzureQueuePushEngineTests
|
||||
{
|
||||
private static readonly Guid _deviceId = Guid.Parse("c4730f80-caaa-4772-97bd-5c0d23a2baa3");
|
||||
private static readonly string _deviceIdentifier = "test_device_identifier";
|
||||
private readonly FakeTimeProvider _fakeTimeProvider;
|
||||
private readonly Core.Settings.GlobalSettings _globalSettings = new();
|
||||
|
||||
public AzureQueuePushNotificationServiceTests()
|
||||
public AzureQueuePushEngineTests()
|
||||
{
|
||||
_fakeTimeProvider = new();
|
||||
_fakeTimeProvider.SetUtcNow(DateTime.UtcNow);
|
||||
@@ -651,31 +651,6 @@ public class AzureQueuePushNotificationServiceTests
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse()
|
||||
{
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Enabled = true,
|
||||
};
|
||||
|
||||
var expectedPayload = new JsonObject
|
||||
{
|
||||
["Type"] = 18,
|
||||
["Payload"] = new JsonObject
|
||||
{
|
||||
["OrganizationId"] = organization.Id,
|
||||
["Enabled"] = organization.Enabled,
|
||||
},
|
||||
};
|
||||
|
||||
await VerifyNotificationAsync(
|
||||
async sut => await sut.PushSyncOrganizationStatusAsync(organization),
|
||||
expectedPayload
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()
|
||||
{
|
||||
@@ -771,12 +746,11 @@ public class AzureQueuePushNotificationServiceTests
|
||||
|
||||
var globalSettings = new Core.Settings.GlobalSettings();
|
||||
|
||||
var sut = new AzureQueuePushNotificationService(
|
||||
var sut = new AzureQueuePushEngine(
|
||||
queueClient,
|
||||
httpContextAccessor,
|
||||
globalSettings,
|
||||
NullLogger<AzureQueuePushNotificationService>.Instance,
|
||||
_fakeTimeProvider
|
||||
NullLogger<AzureQueuePushEngine>.Instance
|
||||
);
|
||||
|
||||
await test(new EngineWrapper(sut, _fakeTimeProvider, _globalSettings.Installation.Id));
|
||||
@@ -2,16 +2,16 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Push.Services;
|
||||
namespace Bit.Core.Test.Platform.Push.Engines;
|
||||
|
||||
public class NotificationsApiPushNotificationServiceTests : PushTestBase
|
||||
public class NotificationsApiPushEngineTests : PushTestBase
|
||||
{
|
||||
public NotificationsApiPushNotificationServiceTests()
|
||||
public NotificationsApiPushEngineTests()
|
||||
{
|
||||
GlobalSettings.BaseServiceUri.InternalNotifications = "https://localhost:7777";
|
||||
GlobalSettings.BaseServiceUri.InternalIdentity = "https://localhost:8888";
|
||||
@@ -21,11 +21,11 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase
|
||||
|
||||
protected override IPushEngine CreateService()
|
||||
{
|
||||
return new NotificationsApiPushNotificationService(
|
||||
return new NotificationsApiPushEngine(
|
||||
HttpClientFactory,
|
||||
GlobalSettings,
|
||||
HttpContextAccessor,
|
||||
NullLogger<NotificationsApiPushNotificationService>.Instance
|
||||
NullLogger<NotificationsApiPushEngine>.Instance
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.NotificationCenter.Enums;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
@@ -22,6 +23,8 @@ using NSubstitute;
|
||||
using RichardSzalay.MockHttp;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Push.Engines;
|
||||
|
||||
public class EngineWrapper(IPushEngine pushEngine, FakeTimeProvider fakeTimeProvider, Guid installationId) : IPushNotificationService
|
||||
{
|
||||
public Guid InstallationId { get; } = installationId;
|
||||
@@ -410,21 +413,6 @@ public abstract class PushTestBase
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse()
|
||||
{
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Enabled = true,
|
||||
};
|
||||
|
||||
await VerifyNotificationAsync(
|
||||
async sut => await sut.PushSyncOrganizationStatusAsync(organization),
|
||||
GetPushSyncOrganizationStatusResponsePayload(organization)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()
|
||||
{
|
||||
@@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
@@ -15,7 +14,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Push.Services;
|
||||
namespace Bit.Core.Test.Platform.Push.Engines;
|
||||
|
||||
public class RelayPushNotificationServiceTests : PushTestBase
|
||||
{
|
||||
@@ -39,12 +38,12 @@ public class RelayPushNotificationServiceTests : PushTestBase
|
||||
|
||||
protected override IPushEngine CreateService()
|
||||
{
|
||||
return new RelayPushNotificationService(
|
||||
return new RelayPushEngine(
|
||||
HttpClientFactory,
|
||||
_deviceRepository,
|
||||
GlobalSettings,
|
||||
HttpContextAccessor,
|
||||
NullLogger<RelayPushNotificationService>.Instance
|
||||
NullLogger<RelayPushEngine>.Instance
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Push;
|
||||
|
||||
public class MultiServicePushNotificationServiceTests
|
||||
{
|
||||
private readonly IPushEngine _fakeEngine1;
|
||||
private readonly IPushEngine _fakeEngine2;
|
||||
|
||||
private readonly MultiServicePushNotificationService _sut;
|
||||
|
||||
public MultiServicePushNotificationServiceTests()
|
||||
{
|
||||
_fakeEngine1 = Substitute.For<IPushEngine>();
|
||||
_fakeEngine2 = Substitute.For<IPushEngine>();
|
||||
|
||||
_sut = new MultiServicePushNotificationService(
|
||||
[_fakeEngine1, _fakeEngine2],
|
||||
NullLogger<MultiServicePushNotificationService>.Instance,
|
||||
new GlobalSettings(),
|
||||
new FakeTimeProvider()
|
||||
);
|
||||
}
|
||||
|
||||
#if DEBUG // This test requires debug code in the sut to work properly
|
||||
[Fact]
|
||||
public async Task PushAsync_CallsAllEngines()
|
||||
{
|
||||
var notification = new PushNotification<object>
|
||||
{
|
||||
Target = NotificationTarget.User,
|
||||
TargetId = Guid.NewGuid(),
|
||||
Type = PushType.AuthRequest,
|
||||
Payload = new { },
|
||||
ExcludeCurrentContext = false,
|
||||
};
|
||||
|
||||
await _sut.PushAsync(notification);
|
||||
|
||||
await _fakeEngine1
|
||||
.Received(1)
|
||||
.PushAsync(Arg.Is<PushNotification<object>>(n => ReferenceEquals(n, notification)));
|
||||
|
||||
await _fakeEngine2
|
||||
.Received(1)
|
||||
.PushAsync(Arg.Is<PushNotification<object>>(n => ReferenceEquals(n, notification)));
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.NotificationHub;
|
||||
namespace Bit.Core.Test.Platform.Push.NotificationHub;
|
||||
|
||||
public class NotificationHubConnectionTests
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -6,7 +6,7 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using static Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.NotificationHub;
|
||||
namespace Bit.Core.Test.Platform.Push.NotificationHub;
|
||||
|
||||
public class NotificationHubPoolTests
|
||||
{
|
||||
@@ -1,11 +1,11 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Microsoft.Azure.NotificationHubs;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.NotificationHub;
|
||||
namespace Bit.Core.Test.Platform.Push.NotificationHub;
|
||||
|
||||
public class NotificationHubProxyTests
|
||||
{
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Context;
|
||||
@@ -7,10 +6,11 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.NotificationCenter.Enums;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.NotificationCenter.AutoFixture;
|
||||
using Bit.Core.Test.Platform.Push.Engines;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@@ -22,7 +22,7 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.NotificationHub;
|
||||
namespace Bit.Core.Test.Platform.Push.NotificationHub;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[NotificationStatusCustomize]
|
||||
@@ -621,11 +621,11 @@ public class NotificationHubPushNotificationServiceTests
|
||||
|
||||
fakeTimeProvider.SetUtcNow(_now);
|
||||
|
||||
var sut = new NotificationHubPushNotificationService(
|
||||
var sut = new NotificationHubPushEngine(
|
||||
installationDeviceRepository,
|
||||
notificationHubPool,
|
||||
httpContextAccessor,
|
||||
NullLogger<NotificationHubPushNotificationService>.Instance,
|
||||
NullLogger<NotificationHubPushEngine>.Instance,
|
||||
globalSettings
|
||||
);
|
||||
|
||||
@@ -676,7 +676,7 @@ public class NotificationHubPushNotificationServiceTests
|
||||
};
|
||||
|
||||
private static async Task AssertSendTemplateNotificationAsync(
|
||||
SutProvider<NotificationHubPushNotificationService> sutProvider, PushType type, object payload, string tag)
|
||||
SutProvider<NotificationHubPushEngine> sutProvider, PushType type, object payload, string tag)
|
||||
{
|
||||
await sutProvider.GetDependency<INotificationHubPool>()
|
||||
.Received(1)
|
||||
@@ -0,0 +1,198 @@
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Repositories.Noop;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Push;
|
||||
|
||||
public class PushServiceCollectionExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddPush_SelfHosted_NoConfig_NoEngines()
|
||||
{
|
||||
var services = Build(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "true" },
|
||||
{ "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() },
|
||||
});
|
||||
|
||||
_ = services.GetRequiredService<IPushNotificationService>();
|
||||
var engines = services.GetServices<IPushEngine>();
|
||||
|
||||
Assert.Empty(engines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPush_SelfHosted_ConfiguredForRelay_RelayEngineAdded()
|
||||
{
|
||||
var services = Build(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "true" },
|
||||
{ "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() },
|
||||
{ "GlobalSettings:Installation:Key", "some_key"},
|
||||
{ "GlobalSettings:PushRelayBaseUri", "https://example.com" },
|
||||
});
|
||||
|
||||
_ = services.GetRequiredService<IPushNotificationService>();
|
||||
var engines = services.GetServices<IPushEngine>();
|
||||
|
||||
var engine = Assert.Single(engines);
|
||||
Assert.IsType<RelayPushEngine>(engine);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPush_SelfHosted_ConfiguredForApi_ApiEngineAdded()
|
||||
{
|
||||
var services = Build(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "true" },
|
||||
{ "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() },
|
||||
{ "GlobalSettings:InternalIdentityKey", "some_key"},
|
||||
{ "GlobalSettings:BaseServiceUri", "https://example.com" },
|
||||
});
|
||||
|
||||
_ = services.GetRequiredService<IPushNotificationService>();
|
||||
var engines = services.GetServices<IPushEngine>();
|
||||
|
||||
var engine = Assert.Single(engines);
|
||||
Assert.IsType<NotificationsApiPushEngine>(engine);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPush_SelfHosted_ConfiguredForRelayAndApi_TwoEnginesAdded()
|
||||
{
|
||||
var services = Build(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "true" },
|
||||
{ "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() },
|
||||
{ "GlobalSettings:Installation:Key", "some_key"},
|
||||
{ "GlobalSettings:PushRelayBaseUri", "https://example.com" },
|
||||
{ "GlobalSettings:InternalIdentityKey", "some_key"},
|
||||
{ "GlobalSettings:BaseServiceUri", "https://example.com" },
|
||||
});
|
||||
|
||||
_ = services.GetRequiredService<IPushNotificationService>();
|
||||
var engines = services.GetServices<IPushEngine>();
|
||||
|
||||
Assert.Collection(
|
||||
engines,
|
||||
e => Assert.IsType<RelayPushEngine>(e),
|
||||
e => Assert.IsType<NotificationsApiPushEngine>(e)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPush_Cloud_NoConfig_AddsNotificationHub()
|
||||
{
|
||||
var services = Build(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "false" },
|
||||
});
|
||||
|
||||
_ = services.GetRequiredService<IPushNotificationService>();
|
||||
var engines = services.GetServices<IPushEngine>();
|
||||
|
||||
var engine = Assert.Single(engines);
|
||||
Assert.IsType<NotificationHubPushEngine>(engine);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPush_Cloud_HasNotificationConnectionString_TwoEngines()
|
||||
{
|
||||
var services = Build(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "false" },
|
||||
{ "GlobalSettings:Notifications:ConnectionString", "UseDevelopmentStorage=true" },
|
||||
});
|
||||
|
||||
_ = services.GetRequiredService<IPushNotificationService>();
|
||||
var engines = services.GetServices<IPushEngine>();
|
||||
|
||||
Assert.Collection(
|
||||
engines,
|
||||
e => Assert.IsType<NotificationHubPushEngine>(e),
|
||||
e => Assert.IsType<AzureQueuePushEngine>(e)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPush_Cloud_CalledTwice_DoesNotAddServicesTwice()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
var config = new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "false" },
|
||||
{ "GlobalSettings:Notifications:ConnectionString", "UseDevelopmentStorage=true" },
|
||||
};
|
||||
|
||||
AddServices(services, config);
|
||||
|
||||
var initialCount = services.Count;
|
||||
|
||||
// Add services again
|
||||
AddServices(services, config);
|
||||
|
||||
Assert.Equal(initialCount, services.Count);
|
||||
}
|
||||
|
||||
private static ServiceProvider Build(Dictionary<string, string?> initialData)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
AddServices(services, initialData);
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static void AddServices(IServiceCollection services, Dictionary<string, string?> initialData)
|
||||
{
|
||||
// A minimal service collection is always expected to have logging, config, and global settings
|
||||
// pre-registered.
|
||||
|
||||
services.AddLogging();
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(initialData)
|
||||
.Build();
|
||||
|
||||
services.TryAddSingleton(config);
|
||||
var globalSettings = new GlobalSettings();
|
||||
config.GetSection("GlobalSettings").Bind(globalSettings);
|
||||
|
||||
services.TryAddSingleton(globalSettings);
|
||||
services.TryAddSingleton<IGlobalSettings>(globalSettings);
|
||||
|
||||
// Temporary until AddPush can add it themselves directly.
|
||||
services.TryAddSingleton<IDeviceRepository, StubDeviceRepository>();
|
||||
|
||||
// Temporary until AddPush can add it themselves directly.
|
||||
services.TryAddSingleton<IInstallationDeviceRepository, InstallationDeviceRepository>();
|
||||
|
||||
services.AddPush(globalSettings);
|
||||
}
|
||||
|
||||
private class StubDeviceRepository : IDeviceRepository
|
||||
{
|
||||
public Task ClearPushTokenAsync(Guid id) => throw new NotImplementedException();
|
||||
public Task<Device> CreateAsync(Device obj) => throw new NotImplementedException();
|
||||
public Task DeleteAsync(Device obj) => throw new NotImplementedException();
|
||||
public Task<Device?> GetByIdAsync(Guid id, Guid userId) => throw new NotImplementedException();
|
||||
public Task<Device?> GetByIdAsync(Guid id) => throw new NotImplementedException();
|
||||
public Task<Device?> GetByIdentifierAsync(string identifier) => throw new NotImplementedException();
|
||||
public Task<Device?> GetByIdentifierAsync(string identifier, Guid userId) => throw new NotImplementedException();
|
||||
public Task<ICollection<Device>> GetManyByUserIdAsync(Guid userId) => throw new NotImplementedException();
|
||||
public Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId) => throw new NotImplementedException();
|
||||
public Task ReplaceAsync(Device obj) => throw new NotImplementedException();
|
||||
public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<Device> devices) => throw new NotImplementedException();
|
||||
public Task UpsertAsync(Device obj) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
64
test/Core.Test/Platform/Push/PushTypeTests.cs
Normal file
64
test/Core.Test/Platform/Push/PushTypeTests.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Push;
|
||||
|
||||
public class PushTypeTests
|
||||
{
|
||||
[Fact]
|
||||
public void AllEnumMembersHaveUniqueValue()
|
||||
{
|
||||
// No enum member should use the same value as another named member.
|
||||
|
||||
var usedNumbers = new HashSet<byte>();
|
||||
var enumMembers = Enum.GetValues<PushType>();
|
||||
|
||||
foreach (var enumMember in enumMembers)
|
||||
{
|
||||
if (!usedNumbers.Add((byte)enumMember))
|
||||
{
|
||||
Assert.Fail($"Enum number value ({(byte)enumMember}) on {enumMember} is already in use.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllEnumMembersHaveNotificationInfoAttribute()
|
||||
{
|
||||
// Every enum member should be annotated with [NotificationInfo]
|
||||
|
||||
foreach (var member in typeof(PushType).GetMembers(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
var notificationInfoAttribute = member.GetCustomAttribute<NotificationInfoAttribute>();
|
||||
if (notificationInfoAttribute is null)
|
||||
{
|
||||
Assert.Fail($"PushType.{member.Name} is missing a required [NotificationInfo(\"team-name\", typeof(MyType))] attribute.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllEnumValuesAreInSequence()
|
||||
{
|
||||
// There should not be any gaps in the numbers defined for an enum, that being if someone last defined 22
|
||||
// the next number used should be 23 not 24 or any other number.
|
||||
|
||||
var sortedValues = Enum.GetValues<PushType>()
|
||||
.Order()
|
||||
.ToArray();
|
||||
|
||||
Debug.Assert(sortedValues.Length > 0);
|
||||
|
||||
var lastValue = sortedValues[0];
|
||||
|
||||
foreach (var value in sortedValues[1..])
|
||||
{
|
||||
var expectedValue = ++lastValue;
|
||||
|
||||
Assert.Equal(expectedValue, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Test.Platform.Push.Services;
|
||||
|
||||
public class MultiServicePushNotificationServiceTests
|
||||
{
|
||||
// TODO: Can add a couple tests here
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Platform.PushRegistration;
|
||||
using Bit.Core.Platform.PushRegistration.Internal;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -0,0 +1,108 @@
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.PushRegistration.Internal;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Repositories.Noop;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.PushRegistration;
|
||||
|
||||
public class PushRegistrationServiceCollectionExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddPushRegistration_Cloud_CreatesNotificationHubRegistrationService()
|
||||
{
|
||||
var services = Build(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "false" },
|
||||
});
|
||||
|
||||
var pushRegistrationService = services.GetRequiredService<IPushRegistrationService>();
|
||||
Assert.IsType<NotificationHubPushRegistrationService>(pushRegistrationService);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPushRegistration_SelfHosted_NoOtherConfig_ReturnsNoopRegistrationService()
|
||||
{
|
||||
var services = Build(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "true" },
|
||||
});
|
||||
|
||||
var pushRegistrationService = services.GetRequiredService<IPushRegistrationService>();
|
||||
Assert.IsType<NoopPushRegistrationService>(pushRegistrationService);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPushRegistration_SelfHosted_RelayConfig_ReturnsRelayRegistrationService()
|
||||
{
|
||||
var services = Build(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "true" },
|
||||
{ "GlobalSettings:PushRelayBaseUri", "https://example.com" },
|
||||
{ "GlobalSettings:Installation:Key", "some_key" },
|
||||
});
|
||||
|
||||
var pushRegistrationService = services.GetRequiredService<IPushRegistrationService>();
|
||||
Assert.IsType<RelayPushRegistrationService>(pushRegistrationService);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPushRegistration_MultipleTimes_NoAdditionalServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
var config = new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SelfHosted", "true" },
|
||||
{ "GlobalSettings:PushRelayBaseUri", "https://example.com" },
|
||||
{ "GlobalSettings:Installation:Key", "some_key" },
|
||||
};
|
||||
|
||||
AddServices(services, config);
|
||||
|
||||
// Add services again
|
||||
services.AddPushRegistration();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
Assert.Single(provider.GetServices<IPushRegistrationService>());
|
||||
}
|
||||
|
||||
private static ServiceProvider Build(Dictionary<string, string?> initialData)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
AddServices(services, initialData);
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static void AddServices(IServiceCollection services, Dictionary<string, string?> initialData)
|
||||
{
|
||||
// A minimal service collection is always expected to have logging, config, and global settings
|
||||
// pre-registered.
|
||||
|
||||
services.AddLogging();
|
||||
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(initialData)
|
||||
.Build();
|
||||
|
||||
services.TryAddSingleton(config);
|
||||
var globalSettings = new GlobalSettings();
|
||||
config.GetSection("GlobalSettings").Bind(globalSettings);
|
||||
|
||||
services.TryAddSingleton(globalSettings);
|
||||
services.TryAddSingleton<IGlobalSettings>(globalSettings);
|
||||
|
||||
|
||||
// Temporary until AddPushRegistration can add it themselves directly.
|
||||
services.TryAddSingleton<IInstallationDeviceRepository, InstallationDeviceRepository>();
|
||||
|
||||
services.AddPushRegistration();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Platform.PushRegistration.Internal;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
@@ -4,8 +4,8 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.PushRegistration;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
@@ -247,11 +247,18 @@ public class HandlebarsMailServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
// Remove this test when we add actual tests. It only proves that
|
||||
// we've properly constructed the system under test.
|
||||
[Fact]
|
||||
public void ServiceExists()
|
||||
public async Task SendSendEmailOtpEmailAsync_SendsEmail()
|
||||
{
|
||||
Assert.NotNull(_sut);
|
||||
// Arrange
|
||||
var email = "test@example.com";
|
||||
var token = "aToken";
|
||||
var subject = string.Format("Your Bitwarden Send verification code is {0}", token);
|
||||
|
||||
// Act
|
||||
await _sut.SendSendEmailOtpEmailAsync(email, token, subject);
|
||||
|
||||
// Assert
|
||||
await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any<MailMessage>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
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.Core.Services;
|
||||
using Bit.Core.Services.Implementations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Services.Implementations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class FeatureRoutedCacheServiceTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
IDictionary<Guid, OrganizationAbility> expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.GetOrganizationAbilitiesAsync()
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetOrganizationAbilitiesAsync();
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetOrganizationAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilitiesAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
IDictionary<Guid, OrganizationAbility> expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.GetOrganizationAbilitiesAsync()
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetOrganizationAbilitiesAsync();
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetOrganizationAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilityAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid orgId,
|
||||
OrganizationAbility expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(orgId)
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetOrganizationAbilityAsync(orgId);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetOrganizationAbilityAsync(orgId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetOrganizationAbilityAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid orgId,
|
||||
OrganizationAbility expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.GetOrganizationAbilityAsync(orgId)
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetOrganizationAbilityAsync(orgId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetOrganizationAbilityAsync(orgId);
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetOrganizationAbilityAsync(orgId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
IDictionary<Guid, ProviderAbility> expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.GetProviderAbilitiesAsync()
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetProviderAbilitiesAsync();
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilitiesAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
IDictionary<Guid, ProviderAbility> expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.GetProviderAbilitiesAsync()
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetProviderAbilitiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetProviderAbilitiesAsync();
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Provider provider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.UpsertProviderAbilityAsync(provider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertProviderAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Provider provider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.UpsertProviderAbilityAsync(provider);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteProviderAbilityAsync(providerId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteProviderAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteProviderAbilityAsync(providerId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.UpsertOrganizationAbilityAsync(organization);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsServiceBusCache(
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
|
||||
var currentCacheService = CreateCurrentCacheMockService();
|
||||
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
var sutProvider = Substitute.For<FeatureRoutedCacheService>(
|
||||
featureService,
|
||||
Substitute.For<IVNextInMemoryApplicationCacheService>(),
|
||||
currentCacheService,
|
||||
Substitute.For<IApplicationCacheServiceBusMessaging>());
|
||||
|
||||
// Act
|
||||
await sutProvider.BaseUpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Assert
|
||||
await currentCacheService
|
||||
.Received(1)
|
||||
.BaseUpsertOrganizationAbilityAsync(organization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Our SUT is using a method that is not part of the IVCurrentInMemoryApplicationCacheService,
|
||||
/// so AutoFixture’s auto-created mock won’t work.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static InMemoryServiceBusApplicationCacheService CreateCurrentCacheMockService()
|
||||
{
|
||||
var currentCacheService = Substitute.For<InMemoryServiceBusApplicationCacheService>(
|
||||
Substitute.For<IOrganizationRepository>(),
|
||||
Substitute.For<IProviderRepository>(),
|
||||
new GlobalSettings
|
||||
{
|
||||
ProjectName = "BitwardenTest",
|
||||
ServiceBus = new GlobalSettings.ServiceBusSettings
|
||||
{
|
||||
ConnectionString = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
|
||||
ApplicationCacheTopicName = "test-topic",
|
||||
ApplicationCacheSubscriptionName = "test-subscription"
|
||||
}
|
||||
});
|
||||
return currentCacheService;
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_AndServiceIsNotServiceBusCache_ThrowsException(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
ExpectedErrorMessage,
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
private static string ExpectedErrorMessage
|
||||
{
|
||||
get => "Expected inMemoryApplicationCacheService to be of type InMemoryServiceBusApplicationCacheService";
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BaseDeleteOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BaseDeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsServiceBusCache(
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
|
||||
var currentCacheService = CreateCurrentCacheMockService();
|
||||
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
var sutProvider = Substitute.For<FeatureRoutedCacheService>(
|
||||
featureService,
|
||||
Substitute.For<IVNextInMemoryApplicationCacheService>(),
|
||||
currentCacheService,
|
||||
Substitute.For<IApplicationCacheServiceBusMessaging>());
|
||||
|
||||
// Act
|
||||
await sutProvider.BaseDeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
await currentCacheService
|
||||
.Received(1)
|
||||
.BaseDeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task
|
||||
BaseDeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_AndServiceIsNotServiceBusCache_ThrowsException(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
ExpectedErrorMessage,
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Tax.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -23,10 +21,6 @@ public class StripePaymentServiceTests
|
||||
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IAutomaticTaxFactory>()
|
||||
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
|
||||
.Returns(new FakeAutomaticTaxStrategy(true));
|
||||
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
@@ -74,10 +68,6 @@ public class StripePaymentServiceTests
|
||||
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IAutomaticTaxFactory>()
|
||||
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
|
||||
.Returns(new FakeAutomaticTaxStrategy(true));
|
||||
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
@@ -125,10 +115,6 @@ public class StripePaymentServiceTests
|
||||
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IAutomaticTaxFactory>()
|
||||
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
|
||||
.Returns(new FakeAutomaticTaxStrategy(true));
|
||||
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
@@ -177,10 +163,6 @@ public class StripePaymentServiceTests
|
||||
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IAutomaticTaxFactory>()
|
||||
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
|
||||
.Returns(new FakeAutomaticTaxStrategy(true));
|
||||
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
@@ -223,4 +205,340 @@ public class StripePaymentServiceTests
|
||||
Assert.Equal(4.08M, actual.TotalAmount);
|
||||
Assert.Equal(4M, actual.TaxableBaseAmount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
Tax = 8,
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
Tax = 8,
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
Tax = 8,
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
Tax = 8,
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_PersonalUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
Tax = 8,
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_BusinessUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
Tax = 8,
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
Tax = 8,
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsTaxExemptReverse(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
Tax = 8,
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
@@ -46,7 +47,41 @@ public class ImportCiphersAsyncCommandTests
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoIndividualVaultAsync_WithBulkResourceCreationServiceEnabled_Success(
|
||||
Guid importingUserId,
|
||||
List<CipherDetails> ciphers,
|
||||
SutProvider<ImportCiphersCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IFolderRepository>()
|
||||
.GetManyByUserIdAsync(importingUserId)
|
||||
.Returns(new List<Folder>());
|
||||
|
||||
var folders = new List<Folder> { new Folder { UserId = importingUserId } };
|
||||
|
||||
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync_vNext(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
@@ -76,7 +111,45 @@ public class ImportCiphersAsyncCommandTests
|
||||
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoIndividualVaultAsync_WithBulkResourceCreationServiceEnabled_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Success(
|
||||
Guid importingUserId,
|
||||
List<CipherDetails> ciphers,
|
||||
SutProvider<ImportCiphersCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(importingUserId)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Disabled,
|
||||
[]));
|
||||
|
||||
sutProvider.GetDependency<IFolderRepository>()
|
||||
.GetManyByUserIdAsync(importingUserId)
|
||||
.Returns(new List<Folder>());
|
||||
|
||||
var folders = new List<Folder> { new Folder { UserId = importingUserId } };
|
||||
|
||||
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync_vNext(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
@@ -120,7 +193,7 @@ public class ImportCiphersAsyncCommandTests
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Enabled,
|
||||
[Guid.NewGuid()]));
|
||||
[new PolicyDetails()]));
|
||||
|
||||
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||
|
||||
@@ -186,6 +259,66 @@ public class ImportCiphersAsyncCommandTests
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoOrganizationalVaultAsync_WithBulkResourceCreationServiceEnabled_Success(
|
||||
Organization organization,
|
||||
Guid importingUserId,
|
||||
OrganizationUser importingOrganizationUser,
|
||||
List<Collection> collections,
|
||||
List<CipherDetails> ciphers,
|
||||
SutProvider<ImportCiphersCommand> sutProvider)
|
||||
{
|
||||
organization.MaxCollections = null;
|
||||
importingOrganizationUser.OrganizationId = organization.Id;
|
||||
|
||||
foreach (var collection in collections)
|
||||
{
|
||||
collection.OrganizationId = organization.Id;
|
||||
}
|
||||
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
cipher.OrganizationId = organization.Id;
|
||||
}
|
||||
|
||||
KeyValuePair<int, int>[] collectionRelationships = {
|
||||
new(0, 0),
|
||||
new(1, 1),
|
||||
new(2, 2)
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(organization.Id, importingUserId)
|
||||
.Returns(importingOrganizationUser);
|
||||
|
||||
// Set up a collection that already exists in the organization
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new List<Collection> { collections[0] });
|
||||
|
||||
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync_vNext(
|
||||
ciphers,
|
||||
Arg.Is<IEnumerable<Collection>>(cols => cols.Count() == collections.Count - 1 &&
|
||||
!cols.Any(c => c.Id == collections[0].Id) && // Check that the collection that already existed in the organization was not added
|
||||
cols.All(c => collections.Any(x => c.Name == x.Name))),
|
||||
Arg.Is<IEnumerable<CollectionCipher>>(c => c.Count() == ciphers.Count),
|
||||
Arg.Is<IEnumerable<CollectionUser>>(cus =>
|
||||
cus.Count() == collections.Count - 1 &&
|
||||
!cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization
|
||||
cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true)));
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoOrganizationalVaultAsync_ThrowsBadRequestException(
|
||||
Organization organization,
|
||||
|
||||
@@ -12,9 +12,11 @@ internal class OrganizationCipher : ICustomization
|
||||
{
|
||||
fixture.Customize<Cipher>(composer => composer
|
||||
.With(c => c.OrganizationId, OrganizationId ?? Guid.NewGuid())
|
||||
.Without(c => c.ArchivedDate)
|
||||
.Without(c => c.UserId));
|
||||
fixture.Customize<CipherDetails>(composer => composer
|
||||
.With(c => c.OrganizationId, Guid.NewGuid())
|
||||
.Without(c => c.ArchivedDate)
|
||||
.Without(c => c.UserId));
|
||||
}
|
||||
}
|
||||
@@ -26,9 +28,11 @@ internal class UserCipher : ICustomization
|
||||
{
|
||||
fixture.Customize<Cipher>(composer => composer
|
||||
.With(c => c.UserId, UserId ?? Guid.NewGuid())
|
||||
.Without(c => c.ArchivedDate)
|
||||
.Without(c => c.OrganizationId));
|
||||
fixture.Customize<CipherDetails>(composer => composer
|
||||
.With(c => c.UserId, Guid.NewGuid())
|
||||
.Without(c => c.ArchivedDate)
|
||||
.Without(c => c.OrganizationId));
|
||||
}
|
||||
}
|
||||
|
||||
49
test/Core.Test/Vault/Commands/ArchiveCiphersCommandTest.cs
Normal file
49
test/Core.Test/Vault/Commands/ArchiveCiphersCommandTest.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
||||
using Bit.Core.Vault.Commands;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Vault.Commands;
|
||||
|
||||
[UserCipherCustomize]
|
||||
[SutProviderCustomize]
|
||||
public class ArchiveCiphersCommandTest
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(true, false, 1, 1, 1)]
|
||||
[BitAutoData(false, false, 1, 0, 1)]
|
||||
[BitAutoData(false, true, 1, 0, 1)]
|
||||
[BitAutoData(true, true, 1, 0, 1)]
|
||||
public async Task ArchiveAsync_Works(
|
||||
bool isEditable, bool hasOrganizationId,
|
||||
int cipherRepoCalls, int resultCountFromQuery, int pushNotificationsCalls,
|
||||
SutProvider<ArchiveCiphersCommand> sutProvider, CipherDetails cipher, User user)
|
||||
{
|
||||
cipher.Edit = isEditable;
|
||||
cipher.OrganizationId = hasOrganizationId ? Guid.NewGuid() : null;
|
||||
|
||||
var cipherList = new List<CipherDetails> { cipher };
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(user.Id).Returns(cipherList);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ArchiveManyAsync([cipher.Id], user.Id);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(cipherRepoCalls).ArchiveAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == resultCountFromQuery
|
||||
&& ids.Count() >= 1
|
||||
? true
|
||||
: ids.All(id => cipherList.Contains(cipher))),
|
||||
user.Id);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(pushNotificationsCalls)
|
||||
.PushSyncCiphersAsync(user.Id);
|
||||
}
|
||||
}
|
||||
49
test/Core.Test/Vault/Commands/UnarchiveCiphersCommandTest.cs
Normal file
49
test/Core.Test/Vault/Commands/UnarchiveCiphersCommandTest.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
||||
using Bit.Core.Vault.Commands;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Vault.Commands;
|
||||
|
||||
[UserCipherCustomize]
|
||||
[SutProviderCustomize]
|
||||
public class UnarchiveCiphersCommandTest
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(true, false, 1, 1, 1)]
|
||||
[BitAutoData(false, false, 1, 0, 1)]
|
||||
[BitAutoData(false, true, 1, 0, 1)]
|
||||
[BitAutoData(true, true, 1, 1, 1)]
|
||||
public async Task UnarchiveAsync_Works(
|
||||
bool isEditable, bool hasOrganizationId,
|
||||
int cipherRepoCalls, int resultCountFromQuery, int pushNotificationsCalls,
|
||||
SutProvider<UnarchiveCiphersCommand> sutProvider, CipherDetails cipher, User user)
|
||||
{
|
||||
cipher.Edit = isEditable;
|
||||
cipher.OrganizationId = hasOrganizationId ? Guid.NewGuid() : null;
|
||||
|
||||
var cipherList = new List<CipherDetails> { cipher };
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(user.Id).Returns(cipherList);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UnarchiveManyAsync([cipher.Id], user.Id);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(cipherRepoCalls).UnarchiveAsync(
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == resultCountFromQuery
|
||||
&& ids.Count() >= 1
|
||||
? true
|
||||
: ids.All(id => cipherList.Contains(cipher))),
|
||||
user.Id);
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(pushNotificationsCalls)
|
||||
.PushSyncCiphersAsync(user.Id);
|
||||
}
|
||||
}
|
||||
@@ -89,4 +89,47 @@ public class OrganizationCiphersQueryTests
|
||||
c.CollectionIds.Any(cId => cId == targetCollectionId) &&
|
||||
c.CollectionIds.Any(cId => cId == otherCollectionId));
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAllOrganizationCiphersExcludingDefaultUserCollections_DelegatesToRepository(
|
||||
Guid organizationId,
|
||||
SutProvider<OrganizationCiphersQuery> sutProvider)
|
||||
{
|
||||
var item1 = new CipherOrganizationDetailsWithCollections(
|
||||
new CipherOrganizationDetails { Id = Guid.NewGuid(), OrganizationId = organizationId },
|
||||
new Dictionary<Guid, IGrouping<Guid, CollectionCipher>>());
|
||||
var item2 = new CipherOrganizationDetailsWithCollections(
|
||||
new CipherOrganizationDetails { Id = Guid.NewGuid(), OrganizationId = organizationId },
|
||||
new Dictionary<Guid, IGrouping<Guid, CollectionCipher>>());
|
||||
|
||||
var repo = sutProvider.GetDependency<ICipherRepository>();
|
||||
repo.GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(organizationId)
|
||||
.Returns(Task.FromResult<IEnumerable<CipherOrganizationDetailsWithCollections>>(
|
||||
new[] { item1, item2 }));
|
||||
|
||||
var actual = (await sutProvider.Sut
|
||||
.GetAllOrganizationCiphersExcludingDefaultUserCollections(organizationId))
|
||||
.ToList();
|
||||
|
||||
Assert.Equal(2, actual.Count);
|
||||
Assert.Same(item1, actual[0]);
|
||||
Assert.Same(item2, actual[1]);
|
||||
|
||||
// and we indeed called the repo once
|
||||
await repo.Received(1)
|
||||
.GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(organizationId);
|
||||
}
|
||||
|
||||
private CipherOrganizationDetailsWithCollections MakeWith(
|
||||
CipherOrganizationDetails baseCipher,
|
||||
params Guid[] cols)
|
||||
{
|
||||
var dict = cols
|
||||
.Select(cid => new CollectionCipher { CipherId = baseCipher.Id, CollectionId = cid })
|
||||
.GroupBy(cc => cc.CipherId)
|
||||
.ToDictionary(g => g.Key, g => g);
|
||||
|
||||
return new CipherOrganizationDetailsWithCollections(baseCipher, dict);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
@@ -173,7 +174,7 @@ public class CipherServiceTests
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(savingUserId)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Enabled,
|
||||
[Guid.NewGuid()]));
|
||||
[new PolicyDetails()]));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null));
|
||||
@@ -673,6 +674,32 @@ public class CipherServiceTests
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("Correct Time")]
|
||||
public async Task ShareManyAsync_CorrectRevisionDate_WithBulkResourceCreationServiceEnabled_Passes(string revisionDateString,
|
||||
SutProvider<CipherService> sutProvider, IEnumerable<CipherDetails> ciphers, Organization organization, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id)
|
||||
.Returns(new Organization
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
MaxStorageGb = 100
|
||||
});
|
||||
|
||||
var cipherInfos = ciphers.Select(c => (c,
|
||||
string.IsNullOrEmpty(revisionDateString) ? null : (DateTime?)c.RevisionDate));
|
||||
var sharingUserId = ciphers.First().UserId.Value;
|
||||
|
||||
await sutProvider.Sut.ShareManyAsync(cipherInfos, organization.Id, collectionIds, sharingUserId);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync_vNext(sharingUserId,
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RestoreAsync_UpdatesUserCipher(Guid restoringUserId, CipherDetails cipher, SutProvider<CipherService> sutProvider)
|
||||
@@ -1093,6 +1120,33 @@ public class CipherServiceTests
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ShareManyAsync_PaidOrgWithAttachment_WithBulkResourceCreationServiceEnabled_Passes(SutProvider<CipherService> sutProvider,
|
||||
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
|
||||
.Returns(new Organization
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
MaxStorageGb = 100
|
||||
});
|
||||
ciphers.FirstOrDefault().Attachments =
|
||||
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
|
||||
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";
|
||||
|
||||
var cipherInfos = ciphers.Select(c => (c,
|
||||
(DateTime?)c.RevisionDate));
|
||||
var sharingUserId = ciphers.First().UserId.Value;
|
||||
|
||||
await sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync_vNext(sharingUserId,
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
private class SaveDetailsAsyncDependencies
|
||||
{
|
||||
public CipherDetails CipherDetails { get; set; }
|
||||
|
||||
108
test/Icons.Test/Services/ChangePasswordUriServiceTests.cs
Normal file
108
test/Icons.Test/Services/ChangePasswordUriServiceTests.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.Net;
|
||||
using Bit.Icons.Services;
|
||||
using Bit.Test.Common.MockedHttpClient;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Icons.Test.Services;
|
||||
|
||||
public class ChangePasswordUriServiceTests : ServiceTestBase<ChangePasswordUriService>
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("https://example.com", "https://example.com:443/.well-known/change-password")]
|
||||
public async Task GetChangePasswordUri_WhenBothChecksPass_ReturnsWellKnownUrl(string domain, string expectedUrl)
|
||||
{
|
||||
// Arrange
|
||||
var mockedHandler = new MockedHttpMessageHandler();
|
||||
|
||||
var nonExistentUrl = $"{domain}/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200";
|
||||
var changePasswordUrl = $"{domain}/.well-known/change-password";
|
||||
|
||||
// Mock the response for the resource-that-should-not-exist request (returns 404)
|
||||
mockedHandler
|
||||
.When(nonExistentUrl)
|
||||
.RespondWith(HttpStatusCode.NotFound)
|
||||
.WithContent(new StringContent("Not found"));
|
||||
|
||||
// Mock the response for the change-password request (returns 200)
|
||||
mockedHandler
|
||||
.When(changePasswordUrl)
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent("Ok"));
|
||||
|
||||
var mockHttpFactory = Substitute.For<IHttpClientFactory>();
|
||||
mockHttpFactory.CreateClient("ChangePasswordUri").Returns(mockedHandler.ToHttpClient());
|
||||
|
||||
var service = new ChangePasswordUriService(mockHttpFactory);
|
||||
|
||||
var result = await service.GetChangePasswordUri(domain);
|
||||
|
||||
Assert.Equal(expectedUrl, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://example.com")]
|
||||
public async Task GetChangePasswordUri_WhenResourceThatShouldNotExistReturns200_ReturnsNull(string domain)
|
||||
{
|
||||
var mockHttpFactory = Substitute.For<IHttpClientFactory>();
|
||||
var mockedHandler = new MockedHttpMessageHandler();
|
||||
|
||||
mockedHandler
|
||||
.When(HttpMethod.Get, $"{domain}/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent("Ok"));
|
||||
|
||||
mockedHandler
|
||||
.When(HttpMethod.Get, $"{domain}/.well-known/change-password")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent("Ok"));
|
||||
|
||||
var httpClient = mockedHandler.ToHttpClient();
|
||||
mockHttpFactory.CreateClient("ChangePasswordUri").Returns(httpClient);
|
||||
|
||||
var service = new ChangePasswordUriService(mockHttpFactory);
|
||||
|
||||
var result = await service.GetChangePasswordUri(domain);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://example.com")]
|
||||
public async Task GetChangePasswordUri_WhenChangePasswordUrlNotFound_ReturnsNull(string domain)
|
||||
{
|
||||
var mockHttpFactory = Substitute.For<IHttpClientFactory>();
|
||||
var mockedHandler = new MockedHttpMessageHandler();
|
||||
|
||||
mockedHandler
|
||||
.When(HttpMethod.Get, $"{domain}/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200")
|
||||
.RespondWith(HttpStatusCode.NotFound)
|
||||
.WithContent(new StringContent("Not found"));
|
||||
|
||||
mockedHandler
|
||||
.When(HttpMethod.Get, $"{domain}/.well-known/change-password")
|
||||
.RespondWith(HttpStatusCode.NotFound)
|
||||
.WithContent(new StringContent("Not found"));
|
||||
|
||||
var httpClient = mockedHandler.ToHttpClient();
|
||||
mockHttpFactory.CreateClient("ChangePasswordUri").Returns(httpClient);
|
||||
|
||||
var service = new ChangePasswordUriService(mockHttpFactory);
|
||||
|
||||
var result = await service.GetChangePasswordUri(domain);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
public async Task GetChangePasswordUri_WhenDomainIsNullOrEmpty_ReturnsNull(string domain)
|
||||
{
|
||||
var mockHttpFactory = Substitute.For<IHttpClientFactory>();
|
||||
var service = new ChangePasswordUriService(mockHttpFactory);
|
||||
|
||||
var result = await service.GetChangePasswordUri(domain);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
@@ -213,8 +213,8 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock password validator to return success
|
||||
var passwordValidator = Substitute.For<ISendPasswordRequestValidator>();
|
||||
passwordValidator.ValidateSendPassword(
|
||||
var passwordValidator = Substitute.For<ISendAuthenticationMethodValidator<ResourcePassword>>();
|
||||
passwordValidator.ValidateRequestAsync(
|
||||
Arg.Any<ExtensionGrantValidationContext>(),
|
||||
Arg.Any<ResourcePassword>(),
|
||||
Arg.Any<Guid>())
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer.Enums;
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Duende.IdentityModel;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation;
|
||||
|
||||
public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
private readonly IdentityApplicationFactory _factory;
|
||||
|
||||
public SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_MissingEmail_ReturnsInvalidRequest()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(["test@example.com"]));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId); // No email
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
Assert.Contains("email is required", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_EmailWithoutOtp_SendsOtpEmail()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
var generatedToken = "123456";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp([email]));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(generatedToken);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
// Mock mail service
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
Assert.Contains("email otp sent", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_ValidOtp_ReturnsAccessToken()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
var otp = "123456";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to validate successfully
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.ValidateTokenAsync(otp, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(true);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: otp);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
Assert.True(response.IsSuccessStatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenResponse.AccessToken, content);
|
||||
Assert.Contains(OidcConstants.TokenResponse.BearerTokenType, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_InvalidOtp_ReturnsInvalidGrant()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
var invalidOtp = "wrong123";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to validate as false
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.ValidateTokenAsync(invalidOtp, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(false);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: invalidOtp);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content);
|
||||
Assert.Contains("email otp is invalid", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAccess_EmailOtpProtectedSend_OtpGenerationFails_ReturnsInvalidRequest()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var email = "test@example.com";
|
||||
|
||||
var client = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(Arg.Any<string>()).Returns(true);
|
||||
services.AddSingleton(featureService);
|
||||
|
||||
var sendAuthQuery = Substitute.For<ISendAuthenticationQuery>();
|
||||
sendAuthQuery.GetAuthenticationMethod(sendId)
|
||||
.Returns(new EmailOtp(new[] { email }));
|
||||
services.AddSingleton(sendAuthQuery);
|
||||
|
||||
// Mock OTP token provider to fail generation
|
||||
var otpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();
|
||||
otpProvider.GenerateTokenAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns((string)null);
|
||||
services.AddSingleton(otpProvider);
|
||||
|
||||
var mailService = Substitute.For<IMailService>();
|
||||
services.AddSingleton(mailService);
|
||||
});
|
||||
}).CreateClient();
|
||||
|
||||
var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsync("/connect/token", requestBody);
|
||||
|
||||
// Assert
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content);
|
||||
}
|
||||
|
||||
private static FormUrlEncodedContent CreateTokenRequestBody(Guid sendId,
|
||||
string sendEmail = null, string emailOtp = null)
|
||||
{
|
||||
var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
var parameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess),
|
||||
new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send ),
|
||||
new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess),
|
||||
new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()),
|
||||
new(SendAccessConstants.TokenRequest.SendId, sendIdBase64)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(sendEmail))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(
|
||||
SendAccessConstants.TokenRequest.Email, sendEmail));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(emailOtp))
|
||||
{
|
||||
parameters.Add(new KeyValuePair<string, string>(
|
||||
SendAccessConstants.TokenRequest.Otp, emailOtp));
|
||||
}
|
||||
|
||||
return new FormUrlEncodedContent(parameters);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Sends;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Identity.IdentityServer.ClientProviders;
|
||||
using Duende.IdentityModel;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Identity.IdentityServer.ClientProviders;
|
||||
using Duende.IdentityModel;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
@@ -17,7 +17,7 @@ using Duende.IdentityServer.Validation;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer;
|
||||
namespace Bit.Identity.Test.IdentityServer.SendAccess;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SendAccessGrantValidatorTests
|
||||
@@ -167,7 +167,7 @@ public class SendAccessGrantValidatorTests
|
||||
// get the claims from the subject
|
||||
var claims = subject.Claims.ToList();
|
||||
Assert.NotEmpty(claims);
|
||||
Assert.Contains(claims, c => c.Type == Claims.SendId && c.Value == sendId.ToString());
|
||||
Assert.Contains(claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString());
|
||||
Assert.Contains(claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString());
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ public class SendAccessGrantValidatorTests
|
||||
.GetAuthenticationMethod(sendId)
|
||||
.Returns(resourcePassword);
|
||||
|
||||
sutProvider.GetDependency<ISendPasswordRequestValidator>()
|
||||
.ValidateSendPassword(context, resourcePassword, sendId)
|
||||
sutProvider.GetDependency<ISendAuthenticationMethodValidator<ResourcePassword>>()
|
||||
.ValidateRequestAsync(context, resourcePassword, sendId)
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
@@ -198,15 +198,16 @@ public class SendAccessGrantValidatorTests
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, context.Result);
|
||||
sutProvider.GetDependency<ISendPasswordRequestValidator>()
|
||||
await sutProvider.GetDependency<ISendAuthenticationMethodValidator<ResourcePassword>>()
|
||||
.Received(1)
|
||||
.ValidateSendPassword(context, resourcePassword, sendId);
|
||||
.ValidateRequestAsync(context, resourcePassword, sendId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_EmailOtpMethod_NotImplemented_ThrowsError(
|
||||
public async Task ValidateAsync_EmailOtpMethod_CallsEmailOtp(
|
||||
[AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
SutProvider<SendAccessGrantValidator> sutProvider,
|
||||
GrantValidationResult expectedResult,
|
||||
Guid sendId,
|
||||
EmailOtp emailOtp)
|
||||
{
|
||||
@@ -216,15 +217,22 @@ public class SendAccessGrantValidatorTests
|
||||
sendId,
|
||||
tokenRequest);
|
||||
|
||||
|
||||
sutProvider.GetDependency<ISendAuthenticationQuery>()
|
||||
.GetAuthenticationMethod(sendId)
|
||||
.Returns(emailOtp);
|
||||
|
||||
sutProvider.GetDependency<ISendAuthenticationMethodValidator<EmailOtp>>()
|
||||
.ValidateRequestAsync(context, emailOtp, sendId)
|
||||
.Returns(expectedResult);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
// Currently the EmailOtp case doesn't set a result, so it should be null
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.ValidateAsync(context));
|
||||
Assert.Equal(expectedResult, context.Result);
|
||||
await sutProvider.GetDependency<ISendAuthenticationMethodValidator<EmailOtp>>()
|
||||
.Received(1)
|
||||
.ValidateRequestAsync(context, emailOtp, sendId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -256,7 +264,7 @@ public class SendAccessGrantValidatorTests
|
||||
public void GrantType_ReturnsCorrectType()
|
||||
{
|
||||
// Arrange & Act
|
||||
var validator = new SendAccessGrantValidator(null!, null!, null!);
|
||||
var validator = new SendAccessGrantValidator(null!, null!, null!, null!);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(CustomGrantTypes.SendAccess, ((IExtensionGrantValidator)validator).GrantType);
|
||||
@@ -0,0 +1,73 @@
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer.SendAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot tests to ensure the string constants in <see cref="SendAccessConstants"/> do not change unintentionally.
|
||||
/// If you change any of these values, please ensure you understand the impact and update the SDK accordingly.
|
||||
/// If you intentionally change any of these values, please update the tests to reflect the new expected values.
|
||||
/// </summary>
|
||||
public class SendConstantsSnapshotTests
|
||||
{
|
||||
[Fact]
|
||||
public void SendAccessError_Constant_HasCorrectValue()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("send_access_error_type", SendAccessConstants.SendAccessError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TokenRequest_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("send_id", SendAccessConstants.TokenRequest.SendId);
|
||||
Assert.Equal("password_hash_b64", SendAccessConstants.TokenRequest.ClientB64HashedPassword);
|
||||
Assert.Equal("email", SendAccessConstants.TokenRequest.Email);
|
||||
Assert.Equal("otp", SendAccessConstants.TokenRequest.Otp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrantValidatorResults_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("valid_send_guid", SendAccessConstants.GrantValidatorResults.ValidSendGuid);
|
||||
Assert.Equal("send_id_required", SendAccessConstants.GrantValidatorResults.SendIdRequired);
|
||||
Assert.Equal("send_id_invalid", SendAccessConstants.GrantValidatorResults.InvalidSendId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PasswordValidatorResults_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("password_hash_b64_invalid", SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch);
|
||||
Assert.Equal("password_hash_b64_required", SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmailOtpValidatorResults_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("email_invalid", SendAccessConstants.EmailOtpValidatorResults.EmailInvalid);
|
||||
Assert.Equal("email_required", SendAccessConstants.EmailOtpValidatorResults.EmailRequired);
|
||||
Assert.Equal("email_and_otp_required_otp_sent", SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent);
|
||||
Assert.Equal("otp_invalid", SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid);
|
||||
Assert.Equal("otp_generation_failed", SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OtpToken_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("send_access", SendAccessConstants.OtpToken.TokenProviderName);
|
||||
Assert.Equal("email_otp", SendAccessConstants.OtpToken.Purpose);
|
||||
Assert.Equal("{0}_{1}", SendAccessConstants.OtpToken.TokenUniqueIdentifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OtpEmail_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("Your Bitwarden Send verification code is {0}", SendAccessConstants.OtpEmail.Subject);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user