Browse Source
And other refactors: - move update organization method to a command - separate authorization from business logic - add tests - move Billing Team logic into their servicepull/6646/head
14 changed files with 1121 additions and 223 deletions
@ -1,41 +1,28 @@
@@ -1,41 +1,28 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below |
||||
#nullable disable |
||||
|
||||
using System.ComponentModel.DataAnnotations; |
||||
using System.ComponentModel.DataAnnotations; |
||||
using System.Text.Json.Serialization; |
||||
using Bit.Core.AdminConsole.Entities; |
||||
using Bit.Core.Models.Data; |
||||
using Bit.Core.Settings; |
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; |
||||
using Bit.Core.Utilities; |
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations; |
||||
|
||||
public class OrganizationUpdateRequestModel |
||||
{ |
||||
[Required] |
||||
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")] |
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))] |
||||
public string Name { get; set; } |
||||
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")] |
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))] |
||||
public string BusinessName { get; set; } |
||||
public string? Name { get; set; } |
||||
|
||||
[EmailAddress] |
||||
[Required] |
||||
[StringLength(256)] |
||||
public string BillingEmail { get; set; } |
||||
public Permissions Permissions { get; set; } |
||||
public OrganizationKeysRequestModel Keys { get; set; } |
||||
public string? BillingEmail { get; set; } |
||||
|
||||
public OrganizationKeysRequestModel? Keys { get; set; } |
||||
|
||||
public virtual Organization ToOrganization(Organization existingOrganization, GlobalSettings globalSettings) |
||||
public OrganizationUpdateRequest ToCommandRequest(Guid organizationId) => new() |
||||
{ |
||||
if (!globalSettings.SelfHosted) |
||||
{ |
||||
// These items come from the license file |
||||
existingOrganization.Name = Name; |
||||
existingOrganization.BusinessName = BusinessName; |
||||
existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim(); |
||||
} |
||||
Keys?.ToOrganization(existingOrganization); |
||||
return existingOrganization; |
||||
} |
||||
OrganizationId = organizationId, |
||||
Name = Name, |
||||
BillingEmail = BillingEmail, |
||||
PublicKey = Keys?.PublicKey, |
||||
EncryptedPrivateKey = Keys?.EncryptedPrivateKey |
||||
}; |
||||
} |
||||
|
||||
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using Bit.Core.AdminConsole.Entities; |
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; |
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; |
||||
|
||||
public interface IOrganizationUpdateCommand |
||||
{ |
||||
/// <summary> |
||||
/// Updates an organization's information in the Bitwarden database and Stripe (if required). |
||||
/// Also optionally updates an organization's public-private keypair if it was not created with one. |
||||
/// On self-host, only the public-private keys will be updated because all other properties are fixed by the license file. |
||||
/// </summary> |
||||
/// <param name="request">The update request containing the details to be updated.</param> |
||||
Task<Organization> UpdateAsync(OrganizationUpdateRequest request); |
||||
} |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
using Bit.Core.AdminConsole.Entities; |
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; |
||||
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; |
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; |
||||
|
||||
public class OrganizationUpdateCommand( |
||||
IOrganizationService organizationService, |
||||
IOrganizationRepository organizationRepository, |
||||
IGlobalSettings globalSettings, |
||||
IOrganizationBillingService organizationBillingService |
||||
) : IOrganizationUpdateCommand |
||||
{ |
||||
public async Task<Organization> UpdateAsync(OrganizationUpdateRequest request) |
||||
{ |
||||
var organization = await organizationRepository.GetByIdAsync(request.OrganizationId); |
||||
if (organization == null) |
||||
{ |
||||
throw new NotFoundException(); |
||||
} |
||||
|
||||
if (globalSettings.SelfHosted) |
||||
{ |
||||
return await UpdateSelfHostedAsync(organization, request); |
||||
} |
||||
|
||||
return await UpdateCloudAsync(organization, request); |
||||
} |
||||
|
||||
private async Task<Organization> UpdateCloudAsync(Organization organization, OrganizationUpdateRequest request) |
||||
{ |
||||
// Store original values for comparison |
||||
var originalName = organization.Name; |
||||
var originalBillingEmail = organization.BillingEmail; |
||||
|
||||
// Apply updates to organization |
||||
organization.UpdateDetails(request); |
||||
organization.BackfillPublicPrivateKeys(request); |
||||
await organizationService.ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated); |
||||
|
||||
// Update billing information in Stripe if required |
||||
await UpdateBillingAsync(organization, originalName, originalBillingEmail); |
||||
|
||||
return organization; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Self-host cannot update the organization details because they are set by the license file. |
||||
/// However, this command does offer a soft migration pathway for organizations without public and private keys. |
||||
/// If we remove this migration code in the future, this command and endpoint can become cloud only. |
||||
/// </summary> |
||||
private async Task<Organization> UpdateSelfHostedAsync(Organization organization, OrganizationUpdateRequest request) |
||||
{ |
||||
organization.BackfillPublicPrivateKeys(request); |
||||
await organizationService.ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated); |
||||
return organization; |
||||
} |
||||
|
||||
private async Task UpdateBillingAsync(Organization organization, string originalName, string? originalBillingEmail) |
||||
{ |
||||
// Update Stripe if name or billing email changed |
||||
var shouldUpdateBilling = originalName != organization.Name || |
||||
originalBillingEmail != organization.BillingEmail; |
||||
|
||||
if (!shouldUpdateBilling || string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
await organizationBillingService.UpdateOrganizationNameAndEmail(organization); |
||||
} |
||||
} |
||||
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
using Bit.Core.AdminConsole.Entities; |
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; |
||||
|
||||
public static class OrganizationUpdateExtensions |
||||
{ |
||||
/// <summary> |
||||
/// Updates the organization name and/or billing email. |
||||
/// Any null property on the request object will be skipped. |
||||
/// </summary> |
||||
public static void UpdateDetails(this Organization organization, OrganizationUpdateRequest request) |
||||
{ |
||||
// These values may or may not be sent by the client depending on the operation being performed. |
||||
// Skip any values not provided. |
||||
if (request.Name is not null) |
||||
{ |
||||
organization.Name = request.Name; |
||||
} |
||||
|
||||
if (request.BillingEmail is not null) |
||||
{ |
||||
organization.BillingEmail = request.BillingEmail.ToLowerInvariant().Trim(); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Updates the organization public and private keys if provided and not already set. |
||||
/// This is legacy code for old organizations that were not created with a public/private keypair. It is a soft |
||||
/// migration that will silently migrate organizations when they change their details. |
||||
/// </summary> |
||||
public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationUpdateRequest request) |
||||
{ |
||||
if (!string.IsNullOrWhiteSpace(request.PublicKey) && string.IsNullOrWhiteSpace(organization.PublicKey)) |
||||
{ |
||||
organization.PublicKey = request.PublicKey; |
||||
} |
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.EncryptedPrivateKey) && string.IsNullOrWhiteSpace(organization.PrivateKey)) |
||||
{ |
||||
organization.PrivateKey = request.EncryptedPrivateKey; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; |
||||
|
||||
/// <summary> |
||||
/// Request model for updating the name, billing email, and/or public-private keys for an organization (legacy migration code). |
||||
/// Any combination of these properties can be updated, so they are optional. If none are specified it will not update anything. |
||||
/// </summary> |
||||
public record OrganizationUpdateRequest |
||||
{ |
||||
/// <summary> |
||||
/// The ID of the organization to update. |
||||
/// </summary> |
||||
public required Guid OrganizationId { get; init; } |
||||
|
||||
/// <summary> |
||||
/// The new organization name to apply (optional, this is skipped if not provided). |
||||
/// </summary> |
||||
public string? Name { get; init; } |
||||
|
||||
/// <summary> |
||||
/// The new billing email address to apply (optional, this is skipped if not provided). |
||||
/// </summary> |
||||
public string? BillingEmail { get; init; } |
||||
|
||||
/// <summary> |
||||
/// The organization's public key to set (optional, only set if not already present on the organization). |
||||
/// </summary> |
||||
public string? PublicKey { get; init; } |
||||
|
||||
/// <summary> |
||||
/// The organization's encrypted private key to set (optional, only set if not already present on the organization). |
||||
/// </summary> |
||||
public string? EncryptedPrivateKey { get; init; } |
||||
} |
||||
@ -0,0 +1,196 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,77 @@
@@ -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 |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,414 @@
@@ -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>()); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue