1
0
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:
✨ Audrey ✨
2025-09-17 13:41:54 -04:00
committed by GitHub
579 changed files with 52348 additions and 4739 deletions

View File

@@ -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);
}

View File

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

View File

@@ -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);
}

View File

@@ -8,8 +8,8 @@ using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Installations;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Repositories;
using NSubstitute;
using Xunit;

View File

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

View File

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

View File

@@ -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(

View File

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

View File

@@ -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: "")]

View File

@@ -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()
{

View File

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

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

View File

@@ -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);

View File

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

View File

@@ -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;

View File

@@ -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);

File diff suppressed because it is too large Load Diff

View File

@@ -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));
}
}

View File

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

View File

@@ -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>()

View File

@@ -8,6 +8,7 @@ using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using NSubstitute.ReceivedExtensions;
@@ -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)

View File

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

View File

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

View File

@@ -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]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);

View File

@@ -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>());
}
}

View File

@@ -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?)>));
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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
{
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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);

View File

@@ -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]

View 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
}

View File

@@ -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
{

View File

@@ -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 =>

View File

@@ -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));
}
}

View File

@@ -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 };

View File

@@ -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]

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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>();

View File

@@ -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);
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -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);
}
}

View File

@@ -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 };
}
}

View File

@@ -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);
}
}

View 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
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));

View File

@@ -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
);
}

View File

@@ -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()
{

View File

@@ -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
);
}

View File

@@ -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
}

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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)

View File

@@ -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();
}
}

View 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);
}
}
}

View File

@@ -1,8 +0,0 @@
#nullable enable
namespace Bit.Core.Test.Platform.Push.Services;
public class MultiServicePushNotificationServiceTests
{
// TODO: Can add a couple tests here
}

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>());
}
}

View File

@@ -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 AutoFixtures auto-created mock wont 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);
}
}

View File

@@ -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
));
}
}

View File

@@ -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,

View File

@@ -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));
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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; }

View 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);
}
}

View File

@@ -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>())

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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