mirror of
https://github.com/bitwarden/server
synced 2025-12-26 21:23:39 +00:00
Merge branch 'main' into jmccannon/ac/pm-27131-auto-confirm-req
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
using System.Net;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
|
||||
|
||||
public class OrganizationsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
private readonly LoginHelper _loginHelper;
|
||||
|
||||
private Organization _organization = null!;
|
||||
private string _ownerEmail = null!;
|
||||
private readonly string _billingEmail = "billing@example.com";
|
||||
private readonly string _organizationName = "Organizations Controller Test Org";
|
||||
|
||||
public OrganizationsControllerTests(ApiApplicationFactory apiFactory)
|
||||
{
|
||||
_factory = apiFactory;
|
||||
_client = _factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ownerEmail = $"org-integration-test-{Guid.NewGuid()}@example.com";
|
||||
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||
|
||||
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||
name: _organizationName,
|
||||
billingEmail: _billingEmail,
|
||||
plan: PlanType.EnterpriseAnnually,
|
||||
ownerEmail: _ownerEmail,
|
||||
passwordManagerSeats: 5,
|
||||
paymentMethod: PaymentMethodType.Card);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_AsOwner_WithoutProvider_CanUpdateOrganization()
|
||||
{
|
||||
// Arrange - Regular organization owner (no provider)
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
var updateRequest = new OrganizationUpdateRequestModel
|
||||
{
|
||||
Name = "Updated Organization Name",
|
||||
BillingEmail = "newbillingemail@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Verify the organization name was updated
|
||||
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
|
||||
Assert.NotNull(updatedOrg);
|
||||
Assert.Equal("Updated Organization Name", updatedOrg.Name);
|
||||
Assert.Equal("newbillingemail@example.com", updatedOrg.BillingEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_AsProvider_CanUpdateOrganization()
|
||||
{
|
||||
// Create and login as a new account to be the provider user (not the owner)
|
||||
var providerUserEmail = $"provider-{Guid.NewGuid()}@example.com";
|
||||
var (token, _) = await _factory.LoginWithNewAccount(providerUserEmail);
|
||||
|
||||
// Set up provider linked to org and ProviderUser entry
|
||||
var provider = await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id,
|
||||
ProviderType.Msp);
|
||||
await ProviderTestHelpers.CreateProviderUserAsync(_factory, provider.Id, providerUserEmail,
|
||||
ProviderUserType.ProviderAdmin);
|
||||
|
||||
await _loginHelper.LoginAsync(providerUserEmail);
|
||||
|
||||
var updateRequest = new OrganizationUpdateRequestModel
|
||||
{
|
||||
Name = "Updated Organization Name",
|
||||
BillingEmail = "newbillingemail@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Verify the organization name was updated
|
||||
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
|
||||
Assert.NotNull(updatedOrg);
|
||||
Assert.Equal("Updated Organization Name", updatedOrg.Name);
|
||||
Assert.Equal("newbillingemail@example.com", updatedOrg.BillingEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_NotMemberOrProvider_CannotUpdateOrganization()
|
||||
{
|
||||
// Create and login as a new account to be unrelated to the org
|
||||
var userEmail = "stranger@example.com";
|
||||
await _factory.LoginWithNewAccount(userEmail);
|
||||
await _loginHelper.LoginAsync(userEmail);
|
||||
|
||||
var updateRequest = new OrganizationUpdateRequestModel
|
||||
{
|
||||
Name = "Updated Organization Name",
|
||||
BillingEmail = "newbillingemail@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
|
||||
// Verify the organization name was not updated
|
||||
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
|
||||
Assert.NotNull(updatedOrg);
|
||||
Assert.Equal(_organizationName, updatedOrg.Name);
|
||||
Assert.Equal(_billingEmail, updatedOrg.BillingEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_AsOwner_WithProvider_CanRenameOrganization()
|
||||
{
|
||||
// Arrange - Create provider and link to organization
|
||||
// The active user is ONLY an org owner, NOT a provider user
|
||||
await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id, ProviderType.Msp);
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
var updateRequest = new OrganizationUpdateRequestModel
|
||||
{
|
||||
Name = "Updated Organization Name",
|
||||
BillingEmail = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Verify the organization name was actually updated
|
||||
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
|
||||
Assert.NotNull(updatedOrg);
|
||||
Assert.Equal("Updated Organization Name", updatedOrg.Name);
|
||||
Assert.Equal(_billingEmail, updatedOrg.BillingEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_AsOwner_WithProvider_CannotChangeBillingEmail()
|
||||
{
|
||||
// Arrange - Create provider and link to organization
|
||||
// The active user is ONLY an org owner, NOT a provider user
|
||||
await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id, ProviderType.Msp);
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
var updateRequest = new OrganizationUpdateRequestModel
|
||||
{
|
||||
Name = "Updated Organization Name",
|
||||
BillingEmail = "updatedbilling@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
|
||||
// Verify the organization was not updated
|
||||
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
|
||||
Assert.NotNull(updatedOrg);
|
||||
Assert.Equal(_organizationName, updatedOrg.Name);
|
||||
Assert.Equal(_billingEmail, updatedOrg.BillingEmail);
|
||||
}
|
||||
}
|
||||
77
test/Api.IntegrationTest/Helpers/ProviderTestHelpers.cs
Normal file
77
test/Api.IntegrationTest/Helpers/ProviderTestHelpers.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.Helpers;
|
||||
|
||||
public static class ProviderTestHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a provider and links it to an organization.
|
||||
/// This does NOT create any provider users.
|
||||
/// </summary>
|
||||
/// <param name="factory">The API application factory</param>
|
||||
/// <param name="organizationId">The organization ID to link to the provider</param>
|
||||
/// <param name="providerType">The type of provider to create</param>
|
||||
/// <param name="providerStatus">The provider status (defaults to Created)</param>
|
||||
/// <returns>The created provider</returns>
|
||||
public static async Task<Provider> CreateProviderAndLinkToOrganizationAsync(
|
||||
ApiApplicationFactory factory,
|
||||
Guid organizationId,
|
||||
ProviderType providerType,
|
||||
ProviderStatusType providerStatus = ProviderStatusType.Created)
|
||||
{
|
||||
var providerRepository = factory.GetService<IProviderRepository>();
|
||||
var providerOrganizationRepository = factory.GetService<IProviderOrganizationRepository>();
|
||||
|
||||
// Create the provider
|
||||
var provider = await providerRepository.CreateAsync(new Provider
|
||||
{
|
||||
Name = $"Test {providerType} Provider",
|
||||
BusinessName = $"Test {providerType} Provider Business",
|
||||
BillingEmail = $"provider-{providerType.ToString().ToLower()}@example.com",
|
||||
Type = providerType,
|
||||
Status = providerStatus,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
// Link the provider to the organization
|
||||
await providerOrganizationRepository.CreateAsync(new ProviderOrganization
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
OrganizationId = organizationId,
|
||||
Key = "test-provider-key"
|
||||
});
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a providerUser for a provider.
|
||||
/// </summary>
|
||||
public static async Task<ProviderUser> CreateProviderUserAsync(
|
||||
ApiApplicationFactory factory,
|
||||
Guid providerId,
|
||||
string userEmail,
|
||||
ProviderUserType providerUserType)
|
||||
{
|
||||
var userRepository = factory.GetService<IUserRepository>();
|
||||
var user = await userRepository.GetByEmailAsync(userEmail);
|
||||
if (user is null)
|
||||
{
|
||||
throw new Exception("No user found in test setup.");
|
||||
}
|
||||
|
||||
var providerUserRepository = factory.GetService<IProviderUserRepository>();
|
||||
return await providerUserRepository.CreateAsync(new ProviderUser
|
||||
{
|
||||
ProviderId = providerId,
|
||||
Status = ProviderUserStatusType.Confirmed,
|
||||
UserId = user.Id,
|
||||
Key = Guid.NewGuid().ToString(),
|
||||
Type = providerUserType
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
@@ -8,9 +7,6 @@ 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;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
@@ -20,7 +16,6 @@ using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
@@ -31,101 +26,23 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
|
||||
public class OrganizationsControllerTests : IDisposable
|
||||
[ControllerCustomize(typeof(OrganizationsController))]
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationsControllerTests
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ISsoConfigService _ssoConfigService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery;
|
||||
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
|
||||
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IOrganizationUpdateKeysCommand _organizationUpdateKeysCommand;
|
||||
private readonly OrganizationsController _sut;
|
||||
|
||||
public OrganizationsControllerTests()
|
||||
{
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
_globalSettings = Substitute.For<GlobalSettings>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_organizationService = Substitute.For<IOrganizationService>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_policyRepository = Substitute.For<IPolicyRepository>();
|
||||
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||
_ssoConfigService = Substitute.For<ISsoConfigService>();
|
||||
_getOrganizationApiKeyQuery = Substitute.For<IGetOrganizationApiKeyQuery>();
|
||||
_rotateOrganizationApiKeyCommand = Substitute.For<IRotateOrganizationApiKeyCommand>();
|
||||
_organizationApiKeyRepository = Substitute.For<IOrganizationApiKeyRepository>();
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_providerRepository = Substitute.For<IProviderRepository>();
|
||||
_providerBillingService = Substitute.For<IProviderBillingService>();
|
||||
_orgDeleteTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<OrgDeleteTokenable>>();
|
||||
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
|
||||
_cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>();
|
||||
_organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>();
|
||||
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
||||
_pricingClient = Substitute.For<IPricingClient>();
|
||||
_organizationUpdateKeysCommand = Substitute.For<IOrganizationUpdateKeysCommand>();
|
||||
|
||||
_sut = new OrganizationsController(
|
||||
_organizationRepository,
|
||||
_organizationUserRepository,
|
||||
_policyRepository,
|
||||
_organizationService,
|
||||
_userService,
|
||||
_currentContext,
|
||||
_ssoConfigRepository,
|
||||
_ssoConfigService,
|
||||
_getOrganizationApiKeyQuery,
|
||||
_rotateOrganizationApiKeyCommand,
|
||||
_createOrganizationApiKeyCommand,
|
||||
_organizationApiKeyRepository,
|
||||
_featureService,
|
||||
_globalSettings,
|
||||
_providerRepository,
|
||||
_providerBillingService,
|
||||
_orgDeleteTokenDataFactory,
|
||||
_removeOrganizationUserCommand,
|
||||
_cloudOrganizationSignUpCommand,
|
||||
_organizationDeleteCommand,
|
||||
_policyRequirementQuery,
|
||||
_pricingClient,
|
||||
_organizationUpdateKeysCommand);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_sut?.Dispose();
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
[Theory, BitAutoData]
|
||||
public async Task OrganizationsController_UserCannotLeaveOrganizationThatProvidesKeyConnector(
|
||||
Guid orgId, User user)
|
||||
SutProvider<OrganizationsController> sutProvider,
|
||||
Guid orgId,
|
||||
User user)
|
||||
{
|
||||
var ssoConfig = new SsoConfig
|
||||
{
|
||||
@@ -140,21 +57,24 @@ public class OrganizationsControllerTests : IDisposable
|
||||
|
||||
user.UsesKeyConnector = true;
|
||||
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { null });
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(orgId).Returns(true);
|
||||
sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IUserService>().GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { null });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Leave(orgId));
|
||||
|
||||
Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.",
|
||||
exception.Message);
|
||||
|
||||
await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().UserLeaveAsync(default, default);
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>().DidNotReceiveWithAnyArgs().UserLeaveAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
[Theory, BitAutoData]
|
||||
public async Task OrganizationsController_UserCannotLeaveOrganizationThatManagesUser(
|
||||
Guid orgId, User user)
|
||||
SutProvider<OrganizationsController> sutProvider,
|
||||
Guid orgId,
|
||||
User user)
|
||||
{
|
||||
var ssoConfig = new SsoConfig
|
||||
{
|
||||
@@ -166,27 +86,34 @@ public class OrganizationsControllerTests : IDisposable
|
||||
Enabled = true,
|
||||
OrganizationId = orgId,
|
||||
};
|
||||
var foundOrg = new Organization();
|
||||
foundOrg.Id = orgId;
|
||||
var foundOrg = new Organization
|
||||
{
|
||||
Id = orgId
|
||||
};
|
||||
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { { foundOrg } });
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(orgId).Returns(true);
|
||||
sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IUserService>().GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { foundOrg });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Leave(orgId));
|
||||
|
||||
Assert.Contains("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.",
|
||||
exception.Message);
|
||||
|
||||
await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default);
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineAutoData(true, false)]
|
||||
[InlineAutoData(false, true)]
|
||||
[InlineAutoData(false, false)]
|
||||
[BitAutoData(true, false)]
|
||||
[BitAutoData(false, true)]
|
||||
[BitAutoData(false, false)]
|
||||
public async Task OrganizationsController_UserCanLeaveOrganizationThatDoesntProvideKeyConnector(
|
||||
bool keyConnectorEnabled, bool userUsesKeyConnector, Guid orgId, User user)
|
||||
bool keyConnectorEnabled,
|
||||
bool userUsesKeyConnector,
|
||||
SutProvider<OrganizationsController> sutProvider,
|
||||
Guid orgId,
|
||||
User user)
|
||||
{
|
||||
var ssoConfig = new SsoConfig
|
||||
{
|
||||
@@ -203,18 +130,19 @@ public class OrganizationsControllerTests : IDisposable
|
||||
|
||||
user.UsesKeyConnector = userUsesKeyConnector;
|
||||
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization>());
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(orgId).Returns(true);
|
||||
sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IUserService>().GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization>());
|
||||
|
||||
await _sut.Leave(orgId);
|
||||
await sutProvider.Sut.Leave(orgId);
|
||||
|
||||
await _removeOrganizationUserCommand.Received(1).UserLeaveAsync(orgId, user.Id);
|
||||
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>().Received(1).UserLeaveAsync(orgId, user.Id);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
[Theory, BitAutoData]
|
||||
public async Task Delete_OrganizationIsConsolidatedBillingClient_ScalesProvidersSeats(
|
||||
SutProvider<OrganizationsController> sutProvider,
|
||||
Provider provider,
|
||||
Organization organization,
|
||||
User user,
|
||||
@@ -228,87 +156,89 @@ public class OrganizationsControllerTests : IDisposable
|
||||
provider.Type = ProviderType.Msp;
|
||||
provider.Status = ProviderStatusType.Billable;
|
||||
|
||||
_currentContext.OrganizationOwner(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, requestModel.Secret).Returns(true);
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider);
|
||||
|
||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||
await sutProvider.Sut.Delete(organizationId.ToString(), requestModel);
|
||||
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
|
||||
_userService.VerifySecretAsync(user, requestModel.Secret).Returns(true);
|
||||
|
||||
_providerRepository.GetByOrganizationIdAsync(organization.Id).Returns(provider);
|
||||
|
||||
await _sut.Delete(organizationId.ToString(), requestModel);
|
||||
|
||||
await _providerBillingService.Received(1)
|
||||
await sutProvider.GetDependency<IProviderBillingService>().Received(1)
|
||||
.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);
|
||||
|
||||
await _organizationDeleteCommand.Received(1).DeleteAsync(organization);
|
||||
await sutProvider.GetDependency<IOrganizationDeleteCommand>().Received(1).DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAutoEnrollStatus_WithPolicyRequirementsEnabled_ReturnsOrganizationAutoEnrollStatus_WithResetPasswordEnabledTrue(
|
||||
SutProvider<OrganizationsController> sutProvider,
|
||||
User user,
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser
|
||||
)
|
||||
OrganizationUser organizationUser)
|
||||
{
|
||||
var policyRequirement = new ResetPasswordPolicyRequirement() { AutoEnrollOrganizations = [organization.Id] };
|
||||
var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [organization.Id] };
|
||||
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_organizationRepository.GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
_organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
||||
_policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);
|
||||
|
||||
var result = await _sut.GetAutoEnrollStatus(organization.Id.ToString());
|
||||
var result = await sutProvider.Sut.GetAutoEnrollStatus(organization.Id.ToString());
|
||||
|
||||
await _userService.Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
||||
await _organizationRepository.Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
||||
await _policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
await sutProvider.GetDependency<IUserService>().Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
||||
await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
|
||||
Assert.True(result.ResetPasswordEnabled);
|
||||
Assert.Equal(result.Id, organization.Id);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAutoEnrollStatus_WithPolicyRequirementsDisabled_ReturnsOrganizationAutoEnrollStatus_WithResetPasswordEnabledTrue(
|
||||
User user,
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser
|
||||
)
|
||||
SutProvider<OrganizationsController> sutProvider,
|
||||
User user,
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser)
|
||||
{
|
||||
var policy = new Policy
|
||||
{
|
||||
Type = PolicyType.ResetPassword,
|
||||
Enabled = true,
|
||||
Data = "{\"AutoEnrollEnabled\": true}",
|
||||
OrganizationId = organization.Id
|
||||
};
|
||||
|
||||
var policy = new Policy() { Type = PolicyType.ResetPassword, Enabled = true, Data = "{\"AutoEnrollEnabled\": true}", OrganizationId = organization.Id };
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword).Returns(policy);
|
||||
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_organizationRepository.GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||
_organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
||||
_policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword).Returns(policy);
|
||||
var result = await sutProvider.Sut.GetAutoEnrollStatus(organization.Id.ToString());
|
||||
|
||||
var result = await _sut.GetAutoEnrollStatus(organization.Id.ToString());
|
||||
|
||||
await _userService.Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
||||
await _organizationRepository.Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
||||
await _policyRequirementQuery.Received(0).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
await _policyRepository.Received(1).GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||
await sutProvider.GetDependency<IUserService>().Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
||||
await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(0).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
await sutProvider.GetDependency<IPolicyRepository>().Received(1).GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||
|
||||
Assert.True(result.ResetPasswordEnabled);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutCollectionManagement_ValidRequest_Success(
|
||||
SutProvider<OrganizationsController> sutProvider,
|
||||
Organization organization,
|
||||
OrganizationCollectionManagementUpdateRequestModel model)
|
||||
{
|
||||
// Arrange
|
||||
_currentContext.OrganizationOwner(organization.Id).Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||
|
||||
var plan = MockPlans.Get(PlanType.EnterpriseAnnually);
|
||||
_pricingClient.GetPlan(Arg.Any<PlanType>()).Returns(plan);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(Arg.Any<PlanType>()).Returns(plan);
|
||||
|
||||
_organizationService
|
||||
sutProvider.GetDependency<IOrganizationService>()
|
||||
.UpdateCollectionManagementSettingsAsync(
|
||||
organization.Id,
|
||||
Arg.Is<OrganizationCollectionManagementSettings>(s =>
|
||||
@@ -319,10 +249,10 @@ public class OrganizationsControllerTests : IDisposable
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
await _sut.PutCollectionManagement(organization.Id, model);
|
||||
await sutProvider.Sut.PutCollectionManagement(organization.Id, model);
|
||||
|
||||
// Assert
|
||||
await _organizationService
|
||||
await sutProvider.GetDependency<IOrganizationService>()
|
||||
.Received(1)
|
||||
.UpdateCollectionManagementSettingsAsync(
|
||||
organization.Id,
|
||||
|
||||
641
test/Billing.Test/Jobs/ReconcileAdditionalStorageJobTests.cs
Normal file
641
test/Billing.Test/Jobs/ReconcileAdditionalStorageJobTests.cs
Normal file
@@ -0,0 +1,641 @@
|
||||
using Bit.Billing.Jobs;
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Quartz;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Billing.Test.Jobs;
|
||||
|
||||
public class ReconcileAdditionalStorageJobTests
|
||||
{
|
||||
private readonly IStripeFacade _stripeFacade;
|
||||
private readonly ILogger<ReconcileAdditionalStorageJob> _logger;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ReconcileAdditionalStorageJob _sut;
|
||||
|
||||
public ReconcileAdditionalStorageJobTests()
|
||||
{
|
||||
_stripeFacade = Substitute.For<IStripeFacade>();
|
||||
_logger = Substitute.For<ILogger<ReconcileAdditionalStorageJob>>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_sut = new ReconcileAdditionalStorageJob(_stripeFacade, _logger, _featureService);
|
||||
}
|
||||
|
||||
#region Feature Flag Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_FeatureFlagDisabled_SkipsProcessing()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
_stripeFacade.DidNotReceiveWithAnyArgs().ListSubscriptionsAutoPagingAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_FeatureFlagEnabled_ProcessesSubscriptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob)
|
||||
.Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode)
|
||||
.Returns(false);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Empty<Subscription>());
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
_stripeFacade.Received(3).ListSubscriptionsAutoPagingAsync(
|
||||
Arg.Is<SubscriptionListOptions>(o => o.Status == "active"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dry Run Mode Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_DryRunMode_DoesNotUpdateSubscriptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(false); // Dry run ON
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_DryRunModeDisabled_UpdatesSubscriptions()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true); // Dry run OFF
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(o => o.Items.Count == 1));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Price ID Processing Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_ProcessesAllThreePriceIds()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(false);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Empty<Subscription>());
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
_stripeFacade.Received(1).ListSubscriptionsAutoPagingAsync(
|
||||
Arg.Is<SubscriptionListOptions>(o => o.Price == "storage-gb-monthly"));
|
||||
_stripeFacade.Received(1).ListSubscriptionsAutoPagingAsync(
|
||||
Arg.Is<SubscriptionListOptions>(o => o.Price == "storage-gb-annually"));
|
||||
_stripeFacade.Received(1).ListSubscriptionsAutoPagingAsync(
|
||||
Arg.Is<SubscriptionListOptions>(o => o.Price == "personal-storage-gb-annually"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Already Processed Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_SubscriptionAlreadyProcessed_SkipsUpdate()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
[StripeConstants.MetadataKeys.StorageReconciled2025] = DateTime.UtcNow.ToString("o")
|
||||
};
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, metadata: metadata);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_SubscriptionWithInvalidProcessedDate_ProcessesSubscription()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
[StripeConstants.MetadataKeys.StorageReconciled2025] = "invalid-date"
|
||||
};
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, metadata: metadata);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_123", Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_SubscriptionWithoutMetadata_ProcessesSubscription()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, metadata: null);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_123", Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Quantity Reduction Logic Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_QuantityGreaterThan4_ReducesBy4()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(o =>
|
||||
o.Items.Count == 1 &&
|
||||
o.Items[0].Quantity == 6 &&
|
||||
o.Items[0].Deleted != true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_QuantityEquals4_DeletesItem()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 4);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(o =>
|
||||
o.Items.Count == 1 &&
|
||||
o.Items[0].Deleted == true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_QuantityLessThan4_DeletesItem()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 2);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(o =>
|
||||
o.Items.Count == 1 &&
|
||||
o.Items[0].Deleted == true));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update Options Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_UpdateOptions_SetsProrationBehaviorToCreateProrations()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(o => o.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_UpdateOptions_SetsReconciledMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(o =>
|
||||
o.Metadata.ContainsKey(StripeConstants.MetadataKeys.StorageReconciled2025) &&
|
||||
!string.IsNullOrEmpty(o.Metadata[StripeConstants.MetadataKeys.StorageReconciled2025])));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Subscription Filtering Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_SubscriptionWithNoItems_SkipsUpdate()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
Items = null
|
||||
};
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_SubscriptionWithDifferentPriceId_SkipsUpdate()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "different-price-id", quantity: 10);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_NullSubscription_SkipsProcessing()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create<Subscription>(null!));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multiple Subscriptions Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_MultipleSubscriptions_ProcessesAll()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription1 = CreateSubscription("sub_1", "storage-gb-monthly", quantity: 10);
|
||||
var subscription2 = CreateSubscription("sub_2", "storage-gb-monthly", quantity: 5);
|
||||
var subscription3 = CreateSubscription("sub_3", "storage-gb-monthly", quantity: 3);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription1, subscription2, subscription3));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(callInfo => callInfo.Arg<string>() switch
|
||||
{
|
||||
"sub_1" => subscription1,
|
||||
"sub_2" => subscription2,
|
||||
"sub_3" => subscription3,
|
||||
_ => null
|
||||
});
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_1", Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_2", Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_3", Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_MixedSubscriptionsWithProcessed_OnlyProcessesUnprocessed()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var processedMetadata = new Dictionary<string, string>
|
||||
{
|
||||
[StripeConstants.MetadataKeys.StorageReconciled2025] = DateTime.UtcNow.ToString("o")
|
||||
};
|
||||
|
||||
var subscription1 = CreateSubscription("sub_1", "storage-gb-monthly", quantity: 10);
|
||||
var subscription2 = CreateSubscription("sub_2", "storage-gb-monthly", quantity: 5, metadata: processedMetadata);
|
||||
var subscription3 = CreateSubscription("sub_3", "storage-gb-monthly", quantity: 3);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription1, subscription2, subscription3));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(callInfo => callInfo.Arg<string>() switch
|
||||
{
|
||||
"sub_1" => subscription1,
|
||||
"sub_3" => subscription3,
|
||||
_ => null
|
||||
});
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_1", Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _stripeFacade.DidNotReceive().UpdateSubscription("sub_2", Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_3", Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_UpdateFails_ContinuesProcessingOthers()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription1 = CreateSubscription("sub_1", "storage-gb-monthly", quantity: 10);
|
||||
var subscription2 = CreateSubscription("sub_2", "storage-gb-monthly", quantity: 5);
|
||||
var subscription3 = CreateSubscription("sub_3", "storage-gb-monthly", quantity: 3);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription1, subscription2, subscription3));
|
||||
|
||||
_stripeFacade.UpdateSubscription("sub_1", Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription1);
|
||||
_stripeFacade.UpdateSubscription("sub_2", Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Throws(new Exception("Stripe API error"));
|
||||
_stripeFacade.UpdateSubscription("sub_3", Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription3);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_1", Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_2", Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_3", Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_UpdateFails_LogsError()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Throws(new Exception("Stripe API error"));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
_logger.Received().Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cancellation Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_CancellationRequested_LogsWarningAndExits()
|
||||
{
|
||||
// Arrange
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.Cancel(); // Cancel immediately
|
||||
var context = CreateJobExecutionContext(cts.Token);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription1 = CreateSubscription("sub_1", "storage-gb-monthly", quantity: 10);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription1));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert - Should not process any subscriptions due to immediate cancellation
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null);
|
||||
_logger.Received().Log(
|
||||
LogLevel.Warning,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static IJobExecutionContext CreateJobExecutionContext(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var context = Substitute.For<IJobExecutionContext>();
|
||||
context.CancellationToken.Returns(cancellationToken);
|
||||
return context;
|
||||
}
|
||||
|
||||
private static Subscription CreateSubscription(
|
||||
string id,
|
||||
string priceId,
|
||||
long? quantity = null,
|
||||
Dictionary<string, string>? metadata = null)
|
||||
{
|
||||
var price = new Price { Id = priceId };
|
||||
var item = new SubscriptionItem
|
||||
{
|
||||
Id = $"si_{id}",
|
||||
Price = price,
|
||||
Quantity = quantity ?? 0
|
||||
};
|
||||
|
||||
return new Subscription
|
||||
{
|
||||
Id = id,
|
||||
Metadata = metadata,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem> { item }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal static class AsyncEnumerable
|
||||
{
|
||||
public static async IAsyncEnumerable<T> Create<T>(params T[] items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static async IAsyncEnumerable<T> Empty<T>()
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Billing.Services;
|
||||
using System.Globalization;
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Billing.Services.Implementations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
@@ -10,7 +11,7 @@ using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Pricing.Premium;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Mail.UpdatedInvoiceIncoming;
|
||||
using Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -117,7 +118,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -126,10 +127,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new() { Id = "si_123", Price = new Price { Id = Prices.PremiumAnnually } }
|
||||
}
|
||||
Data = [new() { Id = "si_123", Price = new Price { Id = Prices.PremiumAnnually } }]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = customerId },
|
||||
@@ -199,7 +197,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -208,10 +206,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new() { Id = priceSubscriptionId, Price = new Price { Id = Prices.PremiumAnnually } }
|
||||
}
|
||||
Data = [new() { Id = priceSubscriptionId, Price = new Price { Id = Prices.PremiumAnnually } }]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer
|
||||
@@ -233,7 +228,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
@@ -272,11 +267,12 @@ public class UpcomingInvoiceHandlerTests
|
||||
o.Discounts[0].Coupon == CouponIDs.Milestone2SubscriptionDiscount &&
|
||||
o.ProrationBehavior == "none"));
|
||||
|
||||
// Verify the updated invoice email was sent
|
||||
// Verify the updated invoice email was sent with correct price
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
Arg.Is<Families2020RenewalMail>(email =>
|
||||
email.ToEmails.Contains("user@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
||||
email.View.MonthlyRenewalPrice == (plan.Seat.Price / 12).ToString("C", new CultureInfo("en-US"))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -291,7 +287,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -307,7 +303,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
var organization = new Organization
|
||||
@@ -375,7 +371,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -395,7 +391,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
var organization = new Organization
|
||||
@@ -469,7 +465,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -489,7 +485,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
var organization = new Organization
|
||||
@@ -560,7 +556,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -576,7 +572,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "UK" },
|
||||
TaxExempt = TaxExempt.None
|
||||
};
|
||||
@@ -622,9 +618,8 @@ public class UpcomingInvoiceHandlerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenUpdateSubscriptionItemPriceIdFails_LogsErrorAndSendsEmail()
|
||||
public async Task HandleAsync_WhenUpdateSubscriptionItemPriceIdFails_LogsErrorAndSendsTraditionalEmail()
|
||||
{
|
||||
// Arrange
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var customerId = "cus_123";
|
||||
@@ -637,7 +632,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -646,10 +641,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new() { Id = priceSubscriptionId, Price = new Price { Id = Prices.PremiumAnnually } }
|
||||
}
|
||||
Data = [new() { Id = priceSubscriptionId, Price = new Price { Id = Prices.PremiumAnnually } }]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Customer = new Customer
|
||||
@@ -671,7 +663,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
@@ -708,11 +700,16 @@ public class UpcomingInvoiceHandlerTests
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
|
||||
// Verify that email was still sent despite the exception
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
email.ToEmails.Contains("user@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
// Verify that traditional email was sent when update fails
|
||||
await _mailService.Received(1).SendInvoiceUpcoming(
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains("user@example.com")),
|
||||
Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),
|
||||
Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),
|
||||
Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),
|
||||
Arg.Is<bool>(b => b == true));
|
||||
|
||||
// Verify renewal email was NOT sent
|
||||
await _mailer.DidNotReceive().SendEmail(Arg.Any<Families2020RenewalMail>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -727,7 +724,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -737,12 +734,12 @@ public class UpcomingInvoiceHandlerTests
|
||||
Items = new StripeList<SubscriptionItem>(),
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = "cus_123" },
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
@@ -784,7 +781,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Free Item" } }
|
||||
Data = [new() { Description = "Free Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -800,7 +797,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
@@ -841,7 +838,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -856,7 +853,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
@@ -885,7 +882,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
Arg.Any<List<string>>(),
|
||||
Arg.Any<bool>());
|
||||
|
||||
await _mailer.DidNotReceive().SendEmail(Arg.Any<UpdatedInvoiceUpcomingMail>());
|
||||
await _mailer.DidNotReceive().SendEmail(Arg.Any<Families2020RenewalMail>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -900,7 +897,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
@@ -915,7 +912,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
@@ -964,7 +961,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -977,8 +974,8 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
@@ -989,7 +986,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
Id = premiumAccessItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePremiumAccessPlanId }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -998,7 +995,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
@@ -1045,9 +1042,10 @@ public class UpcomingInvoiceHandlerTests
|
||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
Arg.Is<Families2020RenewalMail>(email =>
|
||||
email.ToEmails.Contains("org@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
||||
email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1066,7 +1064,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1079,14 +1077,14 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -1095,7 +1093,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
@@ -1156,7 +1154,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1168,14 +1166,14 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -1184,7 +1182,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
@@ -1232,7 +1230,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1244,14 +1242,10 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = "si_pm_123",
|
||||
Price = new Price { Id = familiesPlan.PasswordManager.StripePlanId }
|
||||
}
|
||||
}
|
||||
Data =
|
||||
[
|
||||
new() { Id = "si_pm_123", Price = new Price { Id = familiesPlan.PasswordManager.StripePlanId } }
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -1260,7 +1254,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
@@ -1307,7 +1301,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1319,14 +1313,10 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = "si_different_item",
|
||||
Price = new Price { Id = "different-price-id" }
|
||||
}
|
||||
}
|
||||
Data =
|
||||
[
|
||||
new() { Id = "si_different_item", Price = new Price { Id = "different-price-id" } }
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -1335,7 +1325,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
@@ -1378,7 +1368,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenMilestone3Enabled_AndUpdateFails_LogsError()
|
||||
public async Task HandleAsync_WhenMilestone3Enabled_AndUpdateFails_LogsErrorAndSendsTraditionalEmail()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
|
||||
@@ -1393,7 +1383,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1406,14 +1396,14 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -1422,7 +1412,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
@@ -1463,11 +1453,16 @@ public class UpcomingInvoiceHandlerTests
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
|
||||
// Should still attempt to send email despite the failure
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
email.ToEmails.Contains("org@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
// Should send traditional email when update fails
|
||||
await _mailService.Received(1).SendInvoiceUpcoming(
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains("org@example.com")),
|
||||
Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),
|
||||
Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),
|
||||
Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),
|
||||
Arg.Is<bool>(b => b == true));
|
||||
|
||||
// Verify renewal email was NOT sent
|
||||
await _mailer.DidNotReceive().SendEmail(Arg.Any<Families2020RenewalMail>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1487,7 +1482,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1500,20 +1495,21 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
|
||||
},
|
||||
|
||||
new()
|
||||
{
|
||||
Id = seatAddOnItemId,
|
||||
Price = new Price { Id = "personal-org-seat-annually" },
|
||||
Quantity = 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -1522,7 +1518,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
@@ -1569,9 +1565,10 @@ public class UpcomingInvoiceHandlerTests
|
||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
Arg.Is<Families2020RenewalMail>(email =>
|
||||
email.ToEmails.Contains("org@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
||||
email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1591,7 +1588,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1604,20 +1601,21 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
|
||||
},
|
||||
|
||||
new()
|
||||
{
|
||||
Id = seatAddOnItemId,
|
||||
Price = new Price { Id = "personal-org-seat-annually" },
|
||||
Quantity = 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -1626,7 +1624,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
@@ -1673,9 +1671,10 @@ public class UpcomingInvoiceHandlerTests
|
||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
Arg.Is<Families2020RenewalMail>(email =>
|
||||
email.ToEmails.Contains("org@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
||||
email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1696,7 +1695,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1709,25 +1708,27 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
|
||||
},
|
||||
|
||||
new()
|
||||
{
|
||||
Id = premiumAccessItemId,
|
||||
Price = new Price { Id = families2019Plan.PasswordManager.StripePremiumAccessPlanId }
|
||||
},
|
||||
|
||||
new()
|
||||
{
|
||||
Id = seatAddOnItemId,
|
||||
Price = new Price { Id = "personal-org-seat-annually" },
|
||||
Quantity = 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -1736,7 +1737,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
@@ -1785,9 +1786,10 @@ public class UpcomingInvoiceHandlerTests
|
||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
Arg.Is<Families2020RenewalMail>(email =>
|
||||
email.ToEmails.Contains("org@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
||||
email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1806,7 +1808,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1819,14 +1821,14 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -1835,7 +1837,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
@@ -1895,7 +1897,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
Data = [new() { Description = "Test Item" }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1907,14 +1909,14 @@ public class UpcomingInvoiceHandlerTests
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = passwordManagerItemId,
|
||||
Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -1923,7 +1925,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.5.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0868" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
|
||||
using Bit.Core.Billing.Organizations.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationUpdateCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_WhenValidOrganization_UpdatesOrganization(
|
||||
Guid organizationId,
|
||||
string name,
|
||||
string billingEmail,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.GatewayCustomerId = null; // No Stripe customer, so no billing update
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = name,
|
||||
BillingEmail = billingEmail
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(name, result.Name);
|
||||
Assert.Equal(billingEmail.ToLowerInvariant().Trim(), result.BillingEmail);
|
||||
|
||||
await organizationRepository
|
||||
.Received(1)
|
||||
.GetByIdAsync(Arg.Is<Guid>(id => id == organizationId));
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateOrganizationNameAndEmail(Arg.Any<Organization>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_WhenOrganizationNotFound_ThrowsNotFoundException(
|
||||
Guid organizationId,
|
||||
string name,
|
||||
string billingEmail,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = name,
|
||||
BillingEmail = billingEmail
|
||||
};
|
||||
|
||||
// Act/Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(request));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData((string)null)]
|
||||
public async Task UpdateAsync_WhenGatewayCustomerIdIsNullOrEmpty_SkipsBillingUpdate(
|
||||
string gatewayCustomerId,
|
||||
Guid organizationId,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.Name = "Old Name";
|
||||
organization.GatewayCustomerId = gatewayCustomerId;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = "New Name",
|
||||
BillingEmail = organization.BillingEmail
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal("New Name", result.Name);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateOrganizationNameAndEmail(Arg.Any<Organization>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_WhenKeysProvided_AndNotAlreadySet_SetsKeys(
|
||||
Guid organizationId,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.PublicKey = null;
|
||||
organization.PrivateKey = null;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = organization.Name,
|
||||
BillingEmail = organization.BillingEmail,
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(publicKey, result.PublicKey);
|
||||
Assert.Equal(encryptedPrivateKey, result.PrivateKey);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_WhenKeysProvided_AndAlreadySet_DoesNotOverwriteKeys(
|
||||
Guid organizationId,
|
||||
string newPublicKey,
|
||||
string newEncryptedPrivateKey,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
var existingPublicKey = organization.PublicKey;
|
||||
var existingPrivateKey = organization.PrivateKey;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = organization.Name,
|
||||
BillingEmail = organization.BillingEmail,
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newEncryptedPrivateKey
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(existingPublicKey, result.PublicKey);
|
||||
Assert.Equal(existingPrivateKey, result.PrivateKey);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_UpdatingNameOnly_UpdatesNameAndNotBillingEmail(
|
||||
Guid organizationId,
|
||||
string newName,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.Name = "Old Name";
|
||||
var originalBillingEmail = organization.BillingEmail;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = newName,
|
||||
BillingEmail = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(newName, result.Name);
|
||||
Assert.Equal(originalBillingEmail, result.BillingEmail);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.Received(1)
|
||||
.UpdateOrganizationNameAndEmail(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_UpdatingBillingEmailOnly_UpdatesBillingEmailAndNotName(
|
||||
Guid organizationId,
|
||||
string newBillingEmail,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.BillingEmail = "old@example.com";
|
||||
var originalName = organization.Name;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = null,
|
||||
BillingEmail = newBillingEmail
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(originalName, result.Name);
|
||||
Assert.Equal(newBillingEmail.ToLowerInvariant().Trim(), result.BillingEmail);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.Received(1)
|
||||
.UpdateOrganizationNameAndEmail(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_WhenNoChanges_PreservesBothFields(
|
||||
Guid organizationId,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
var organizationService = sutProvider.GetDependency<IOrganizationService>();
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
|
||||
organization.Id = organizationId;
|
||||
var originalName = organization.Name;
|
||||
var originalBillingEmail = organization.BillingEmail;
|
||||
|
||||
organizationRepository
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = null,
|
||||
BillingEmail = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.Id);
|
||||
Assert.Equal(originalName, result.Name);
|
||||
Assert.Equal(originalBillingEmail, result.BillingEmail);
|
||||
|
||||
await organizationService
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(
|
||||
result,
|
||||
EventType.Organization_Updated);
|
||||
await organizationBillingService
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateOrganizationNameAndEmail(Arg.Any<Organization>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_SelfHosted_OnlyUpdatesKeysNotOrganizationDetails(
|
||||
Guid organizationId,
|
||||
string newName,
|
||||
string newBillingEmail,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey,
|
||||
Organization organization,
|
||||
SutProvider<OrganizationUpdateCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();
|
||||
var globalSettings = sutProvider.GetDependency<IGlobalSettings>();
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
|
||||
globalSettings.SelfHosted.Returns(true);
|
||||
|
||||
organization.Id = organizationId;
|
||||
organization.Name = "Original Name";
|
||||
organization.BillingEmail = "original@example.com";
|
||||
organization.PublicKey = null;
|
||||
organization.PrivateKey = null;
|
||||
|
||||
organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||
|
||||
var request = new OrganizationUpdateRequest
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = newName, // Should be ignored
|
||||
BillingEmail = newBillingEmail, // Should be ignored
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Original Name", result.Name); // Not changed
|
||||
Assert.Equal("original@example.com", result.BillingEmail); // Not changed
|
||||
Assert.Equal(publicKey, result.PublicKey); // Changed
|
||||
Assert.Equal(encryptedPrivateKey, result.PrivateKey); // Changed
|
||||
|
||||
await organizationBillingService
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpdateOrganizationNameAndEmail(Arg.Any<Organization>());
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using Bit.Test.Common.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
@@ -25,7 +26,6 @@ public class EventIntegrationHandlerTests
|
||||
private const string _templateWithOrganization = "Org: #OrganizationName#";
|
||||
private const string _templateWithUser = "#UserName#, #UserEmail#, #UserType#";
|
||||
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#, #ActingUserType#";
|
||||
private static readonly Guid _groupId = Guid.NewGuid();
|
||||
private static readonly Guid _organizationId = Guid.NewGuid();
|
||||
private static readonly Uri _uri = new Uri("https://localhost");
|
||||
private static readonly Uri _uri2 = new Uri("https://example.com");
|
||||
@@ -113,6 +113,232 @@ public class EventIntegrationHandlerTests
|
||||
return [config];
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_ActingUserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
eventMessage.ActingUserId ??= Guid.NewGuid();
|
||||
|
||||
cache.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
).Returns(actingUser);
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
|
||||
|
||||
await cache.Received(1).GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
|
||||
Assert.Equal(actingUser, context.ActingUser);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_ActingUserIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
eventMessage.ActingUserId = null;
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
Assert.Null(context.ActingUser);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_ActingUserOrganizationIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId = null;
|
||||
eventMessage.ActingUserId ??= Guid.NewGuid();
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
Assert.Null(context.ActingUser);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_GroupIdPresent_UsesCache(EventMessage eventMessage, Group group)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.GroupId ??= Guid.NewGuid();
|
||||
|
||||
cache.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
|
||||
).Returns(group);
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
|
||||
|
||||
await cache.Received(1).GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
|
||||
);
|
||||
Assert.Equal(group, context.Group);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_GroupIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
eventMessage.GroupId = null;
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
|
||||
);
|
||||
Assert.Null(context.Group);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_OrganizationIdPresent_UsesCache(EventMessage eventMessage, Organization organization)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
|
||||
cache.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
|
||||
).Returns(organization);
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
|
||||
|
||||
await cache.Received(1).GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
|
||||
);
|
||||
Assert.Equal(organization, context.Organization);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_OrganizationIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId = null;
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
|
||||
);
|
||||
Assert.Null(context.Organization);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_UserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
eventMessage.UserId ??= Guid.NewGuid();
|
||||
|
||||
cache.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
).Returns(userDetails);
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
|
||||
|
||||
await cache.Received(1).GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
|
||||
Assert.Equal(userDetails, context.User);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_UserIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId = null;
|
||||
eventMessage.UserId ??= Guid.NewGuid();
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
|
||||
Assert.Null(context.User);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_OrganizationUserIdNull_SkipsCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
eventMessage.UserId = null;
|
||||
|
||||
var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
|
||||
Assert.Null(context.User);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task BuildContextAsync_NoSpecialTokens_DoesNotCallAnyCache(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||
|
||||
eventMessage.ActingUserId ??= Guid.NewGuid();
|
||||
eventMessage.GroupId ??= Guid.NewGuid();
|
||||
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||
eventMessage.UserId ??= Guid.NewGuid();
|
||||
|
||||
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateBase);
|
||||
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()
|
||||
);
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()
|
||||
);
|
||||
await cache.DidNotReceive().GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
|
||||
@@ -176,99 +402,6 @@ public class EventIntegrationHandlerTests
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||
var user = Substitute.For<OrganizationUserUserDetails>();
|
||||
user.Email = "test@example.com";
|
||||
user.Name = "Test";
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(user);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}");
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), eventMessage.ActingUserId ?? Guid.Empty);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_GroupTemplate_LoadsGroupFromRepository(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
|
||||
var group = Substitute.For<Group>();
|
||||
group.Name = "Test";
|
||||
eventMessage.GroupId = _groupId;
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(group);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
|
||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Group: {group.Name}");
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).GetByIdAsync(eventMessage.GroupId ?? Guid.Empty);
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
|
||||
var organization = Substitute.For<Organization>();
|
||||
organization.Name = "Test";
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(organization);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
|
||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Org: {organization.Name}");
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var user = Substitute.For<OrganizationUserUserDetails>();
|
||||
user.Email = "test@example.com";
|
||||
user.Name = "Test";
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(user);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}");
|
||||
|
||||
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), eventMessage.UserId ?? Guid.Empty);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -40,22 +41,55 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IMailService>()
|
||||
.SendRegistrationVerificationEmailAsync(email, Arg.Any<string>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var mockedToken = "token";
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.Protect(Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(mockedToken);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails);
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendRegistrationVerificationEmailAsync(email, mockedToken);
|
||||
.SendRegistrationVerificationEmailAsync(email, mockedToken, null);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendVerificationEmailForRegistrationCommand_WhenFromMarketingIsPremium_SendsEmailWithMarketingParameterAndReturnsNull(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
|
||||
string email, string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByEmailAsync(email)
|
||||
.ReturnsNull();
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.EnableEmailVerification = true;
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
var mockedToken = "token";
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
||||
.Protect(Arg.Any<RegistrationEmailVerificationTokenable>())
|
||||
.Returns(mockedToken);
|
||||
|
||||
var fromMarketing = MarketingInitiativeConstants.Premium;
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, fromMarketing);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendRegistrationVerificationEmailAsync(email, mockedToken, fromMarketing);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
@@ -87,12 +121,12 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
.Returns(mockedToken);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails);
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendRegistrationVerificationEmailAsync(email, mockedToken);
|
||||
.SendRegistrationVerificationEmailAsync(email, mockedToken, null);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
@@ -124,7 +158,7 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
.Returns(mockedToken);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails);
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(mockedToken, result);
|
||||
@@ -140,7 +174,7 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
.DisableUserRegistration = true;
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails));
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -166,7 +200,7 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
.Returns(false);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails));
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -177,7 +211,7 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run(null, name, receiveMarketingEmails));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run(null, name, receiveMarketingEmails, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -187,7 +221,7 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
{
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.DisableUserRegistration = false;
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run("", name, receiveMarketingEmails));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run("", name, receiveMarketingEmails, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -210,7 +244,7 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
.Returns(true);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails));
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));
|
||||
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
|
||||
}
|
||||
|
||||
@@ -246,7 +280,7 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
.Returns(mockedToken);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails);
|
||||
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(mockedToken, result);
|
||||
@@ -270,7 +304,7 @@ public class SendVerificationEmailForRegistrationCommandTests
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.Run(email, name, receiveMarketingEmails));
|
||||
sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));
|
||||
Assert.Equal("Invalid email address format.", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
@@ -353,4 +354,97 @@ public class OrganizationBillingServiceTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_UpdatesStripeCustomer(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
organization.Name = "Short name";
|
||||
|
||||
CustomerUpdateOptions capturedOptions = null;
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.CustomerUpdateAsync(
|
||||
Arg.Is<string>(id => id == organization.GatewayCustomerId),
|
||||
Arg.Do<CustomerUpdateOptions>(options => capturedOptions = options))
|
||||
.Returns(new Customer());
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.Received(1)
|
||||
.CustomerUpdateAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
|
||||
Assert.NotNull(capturedOptions);
|
||||
Assert.Equal(organization.BillingEmail, capturedOptions.Email);
|
||||
Assert.Equal(organization.DisplayName(), capturedOptions.Description);
|
||||
Assert.NotNull(capturedOptions.InvoiceSettings);
|
||||
Assert.NotNull(capturedOptions.InvoiceSettings.CustomFields);
|
||||
Assert.Single(capturedOptions.InvoiceSettings.CustomFields);
|
||||
|
||||
var customField = capturedOptions.InvoiceSettings.CustomFields.First();
|
||||
Assert.Equal(organization.SubscriberType(), customField.Name);
|
||||
Assert.Equal(organization.DisplayName(), customField.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenNameIsLong_TruncatesTo30Characters(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.Name = "This is a very long organization name that exceeds thirty characters";
|
||||
|
||||
CustomerUpdateOptions capturedOptions = null;
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.CustomerUpdateAsync(
|
||||
Arg.Is<string>(id => id == organization.GatewayCustomerId),
|
||||
Arg.Do<CustomerUpdateOptions>(options => capturedOptions = options))
|
||||
.Returns(new Customer());
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.Received(1)
|
||||
.CustomerUpdateAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Any<CustomerUpdateOptions>());
|
||||
|
||||
Assert.NotNull(capturedOptions);
|
||||
Assert.NotNull(capturedOptions.InvoiceSettings);
|
||||
Assert.NotNull(capturedOptions.InvoiceSettings.CustomFields);
|
||||
|
||||
var customField = capturedOptions.InvoiceSettings.CustomFields.First();
|
||||
Assert.Equal(30, customField.Value.Length);
|
||||
|
||||
var expectedCustomFieldDisplayName = "This is a very long organizati";
|
||||
Assert.Equal(expectedCustomFieldDisplayName, customField.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateOrganizationNameAndEmail_WhenGatewayCustomerIdIsNull_ThrowsBillingException(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.GatewayCustomerId = null;
|
||||
organization.Name = "Test Organization";
|
||||
organization.BillingEmail = "billing@example.com";
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BillingException>(
|
||||
() => sutProvider.Sut.UpdateOrganizationNameAndEmail(organization));
|
||||
|
||||
Assert.Contains("Cannot update an organization in Stripe without a GatewayCustomerId.", exception.Response);
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
|
||||
public class EventIntegrationsCacheConstantsTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void BuildCacheKeyForGroup_ReturnsExpectedKey(Guid groupId)
|
||||
{
|
||||
var expected = $"Group:{groupId:N}";
|
||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(groupId);
|
||||
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void BuildCacheKeyForOrganization_ReturnsExpectedKey(Guid orgId)
|
||||
{
|
||||
var expected = $"Organization:{orgId:N}";
|
||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(orgId);
|
||||
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void BuildCacheKeyForOrganizationUser_ReturnsExpectedKey(Guid orgId, Guid userId)
|
||||
{
|
||||
var expected = $"OrganizationUserUserDetails:{orgId:N}:{userId:N}";
|
||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, userId);
|
||||
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CacheName_ReturnsExpected()
|
||||
{
|
||||
Assert.Equal("EventIntegrations", EventIntegrationsCacheConstants.CacheName);
|
||||
}
|
||||
}
|
||||
@@ -241,7 +241,7 @@ public class AccountsControllerTests : IDisposable
|
||||
|
||||
var token = "fakeToken";
|
||||
|
||||
_sendVerificationEmailForRegistrationCommand.Run(email, name, receiveMarketingEmails).Returns(token);
|
||||
_sendVerificationEmailForRegistrationCommand.Run(email, name, receiveMarketingEmails, null).Returns(token);
|
||||
|
||||
// Act
|
||||
var result = await _sut.PostRegisterSendVerificationEmail(model);
|
||||
@@ -264,7 +264,7 @@ public class AccountsControllerTests : IDisposable
|
||||
ReceiveMarketingEmails = receiveMarketingEmails
|
||||
};
|
||||
|
||||
_sendVerificationEmailForRegistrationCommand.Run(email, name, receiveMarketingEmails).ReturnsNull();
|
||||
_sendVerificationEmailForRegistrationCommand.Run(email, name, receiveMarketingEmails, null).ReturnsNull();
|
||||
|
||||
// Act
|
||||
var result = await _sut.PostRegisterSendVerificationEmail(model);
|
||||
@@ -274,6 +274,55 @@ public class AccountsControllerTests : IDisposable
|
||||
Assert.Equal(204, noContentResult.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostRegisterSendEmailVerification_WhenFeatureFlagEnabled_PassesFromMarketingToCommandAsync(
|
||||
string email, string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
var fromMarketing = MarketingInitiativeConstants.Premium;
|
||||
var model = new RegisterSendVerificationEmailRequestModel
|
||||
{
|
||||
Email = email,
|
||||
Name = name,
|
||||
ReceiveMarketingEmails = receiveMarketingEmails,
|
||||
FromMarketing = fromMarketing,
|
||||
};
|
||||
|
||||
_featureService.IsEnabled(FeatureFlagKeys.MarketingInitiatedPremiumFlow).Returns(true);
|
||||
|
||||
// Act
|
||||
await _sut.PostRegisterSendVerificationEmail(model);
|
||||
|
||||
// Assert
|
||||
await _sendVerificationEmailForRegistrationCommand.Received(1)
|
||||
.Run(email, name, receiveMarketingEmails, fromMarketing);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostRegisterSendEmailVerification_WhenFeatureFlagDisabled_PassesNullFromMarketingToCommandAsync(
|
||||
string email, string name, bool receiveMarketingEmails)
|
||||
{
|
||||
// Arrange
|
||||
var model = new RegisterSendVerificationEmailRequestModel
|
||||
{
|
||||
Email = email,
|
||||
Name = name,
|
||||
ReceiveMarketingEmails = receiveMarketingEmails,
|
||||
FromMarketing = MarketingInitiativeConstants.Premium, // model includes FromMarketing: "premium"
|
||||
};
|
||||
|
||||
_featureService.IsEnabled(FeatureFlagKeys.MarketingInitiatedPremiumFlow).Returns(false);
|
||||
|
||||
// Act
|
||||
await _sut.PostRegisterSendVerificationEmail(model);
|
||||
|
||||
// Assert
|
||||
await _sendVerificationEmailForRegistrationCommand.Received(1)
|
||||
.Run(email, name, receiveMarketingEmails, null); // fromMarketing gets ignored and null gets passed
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostRegisterFinish_WhenGivenOrgInvite_ShouldRegisterUser(
|
||||
string email, string masterPasswordHash, string orgInviteToken, Guid organizationUserId, string userSymmetricKey,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -35,7 +35,7 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
// This allows us to use the official registration flow
|
||||
SubstituteService<IMailService>(service =>
|
||||
{
|
||||
service.SendRegistrationVerificationEmailAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
service.SendRegistrationVerificationEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.ReturnsForAnyArgs(Task.CompletedTask)
|
||||
.AndDoes(call =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user