diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs
index d0411f9bc5..b4eecdba0f 100644
--- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs
+++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs
@@ -5,6 +5,7 @@ using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Organizations;
using Bit.Api.Utilities;
+using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Billing.Organizations.Commands;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Organizations.Queries;
@@ -28,7 +29,7 @@ public class SelfHostedOrganizationLicensesController : Controller
private readonly ICurrentContext _currentContext;
private readonly IGetSelfHostedOrganizationLicenseQuery _getSelfHostedOrganizationLicenseQuery;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
- private readonly IOrganizationService _organizationService;
+ private readonly ISelfHostedOrganizationSignUpCommand _selfHostedOrganizationSignUpCommand;
private readonly IOrganizationRepository _organizationRepository;
private readonly IUserService _userService;
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
@@ -37,7 +38,7 @@ public class SelfHostedOrganizationLicensesController : Controller
ICurrentContext currentContext,
IGetSelfHostedOrganizationLicenseQuery getSelfHostedOrganizationLicenseQuery,
IOrganizationConnectionRepository organizationConnectionRepository,
- IOrganizationService organizationService,
+ ISelfHostedOrganizationSignUpCommand selfHostedOrganizationSignUpCommand,
IOrganizationRepository organizationRepository,
IUserService userService,
IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand)
@@ -45,7 +46,7 @@ public class SelfHostedOrganizationLicensesController : Controller
_currentContext = currentContext;
_getSelfHostedOrganizationLicenseQuery = getSelfHostedOrganizationLicenseQuery;
_organizationConnectionRepository = organizationConnectionRepository;
- _organizationService = organizationService;
+ _selfHostedOrganizationSignUpCommand = selfHostedOrganizationSignUpCommand;
_organizationRepository = organizationRepository;
_userService = userService;
_updateOrganizationLicenseCommand = updateOrganizationLicenseCommand;
@@ -66,7 +67,7 @@ public class SelfHostedOrganizationLicensesController : Controller
throw new BadRequestException("Invalid license");
}
- var result = await _organizationService.SignUpAsync(license, user, model.Key,
+ var result = await _selfHostedOrganizationSignUpCommand.SignUpAsync(license, user, model.Key,
model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey);
return new OrganizationResponseModel(result.Item1, null);
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/ISelfHostedOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/ISelfHostedOrganizationSignUpCommand.cs
new file mode 100644
index 0000000000..2686384a34
--- /dev/null
+++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/ISelfHostedOrganizationSignUpCommand.cs
@@ -0,0 +1,15 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.Billing.Organizations.Models;
+using Bit.Core.Entities;
+
+namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
+
+public interface ISelfHostedOrganizationSignUpCommand
+{
+ ///
+ /// Create a new organization on a self-hosted instance
+ ///
+ Task<(Organization organization, OrganizationUser? organizationUser)> SignUpAsync(
+ OrganizationLicense license, User owner, string ownerKey,
+ string? collectionName, string publicKey, string privateKey);
+}
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommand.cs
new file mode 100644
index 0000000000..c52b7c10c9
--- /dev/null
+++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommand.cs
@@ -0,0 +1,216 @@
+using System.Text.Json;
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
+using Bit.Core.AdminConsole.Services;
+using Bit.Core.Billing.Organizations.Models;
+using Bit.Core.Billing.Services;
+using Bit.Core.Entities;
+using Bit.Core.Enums;
+using Bit.Core.Exceptions;
+using Bit.Core.Models.Data;
+using Bit.Core.Platform.Push;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
+using Bit.Core.Settings;
+using Bit.Core.Utilities;
+
+namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
+
+public class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUpCommand
+{
+ private readonly IOrganizationRepository _organizationRepository;
+ private readonly IOrganizationUserRepository _organizationUserRepository;
+ private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
+ private readonly IApplicationCacheService _applicationCacheService;
+ private readonly ICollectionRepository _collectionRepository;
+ private readonly IPushRegistrationService _pushRegistrationService;
+ private readonly IPushNotificationService _pushNotificationService;
+ private readonly IDeviceRepository _deviceRepository;
+ private readonly ILicensingService _licensingService;
+ private readonly IPolicyService _policyService;
+ private readonly IGlobalSettings _globalSettings;
+ private readonly IPaymentService _paymentService;
+
+ public SelfHostedOrganizationSignUpCommand(
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository,
+ IOrganizationApiKeyRepository organizationApiKeyRepository,
+ IApplicationCacheService applicationCacheService,
+ ICollectionRepository collectionRepository,
+ IPushRegistrationService pushRegistrationService,
+ IPushNotificationService pushNotificationService,
+ IDeviceRepository deviceRepository,
+ ILicensingService licensingService,
+ IPolicyService policyService,
+ IGlobalSettings globalSettings,
+ IPaymentService paymentService)
+ {
+ _organizationRepository = organizationRepository;
+ _organizationUserRepository = organizationUserRepository;
+ _organizationApiKeyRepository = organizationApiKeyRepository;
+ _applicationCacheService = applicationCacheService;
+ _collectionRepository = collectionRepository;
+ _pushRegistrationService = pushRegistrationService;
+ _pushNotificationService = pushNotificationService;
+ _deviceRepository = deviceRepository;
+ _licensingService = licensingService;
+ _policyService = policyService;
+ _globalSettings = globalSettings;
+ _paymentService = paymentService;
+ }
+
+ public async Task<(Organization organization, OrganizationUser? organizationUser)> SignUpAsync(
+ OrganizationLicense license, User owner, string ownerKey, string? collectionName, string publicKey,
+ string privateKey)
+ {
+ if (license.LicenseType != LicenseType.Organization)
+ {
+ throw new BadRequestException("Premium licenses cannot be applied to an organization. " +
+ "Upload this license from your personal account settings page.");
+ }
+
+ var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);
+ var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception);
+
+ if (!canUse)
+ {
+ throw new BadRequestException(exception);
+ }
+
+ var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();
+ if (enabledOrgs.Any(o => string.Equals(o.LicenseKey, license.LicenseKey)))
+ {
+ throw new BadRequestException("License is already in use by another organization.");
+ }
+
+ await ValidateSignUpPoliciesAsync(owner.Id);
+
+ var organization = claimsPrincipal != null
+ // If the ClaimsPrincipal exists (there's a token on the license), use it to build the organization.
+ ? OrganizationFactory.Create(owner, claimsPrincipal, publicKey, privateKey)
+ // If there's no ClaimsPrincipal (there's no token on the license), use the license to build the organization.
+ : OrganizationFactory.Create(owner, license, publicKey, privateKey);
+
+ var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
+
+ var dir = $"{_globalSettings.LicenseDirectory}/organization";
+ Directory.CreateDirectory(dir);
+ await using var fs = new FileStream(Path.Combine(dir, $"{organization.Id}.json"), FileMode.Create);
+ await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);
+ return (result.organization, result.organizationUser);
+ }
+
+ private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
+ {
+ var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
+ if (anySingleOrgPolicies)
+ {
+ throw new BadRequestException("You may not create an organization. You belong to an organization " +
+ "which has a policy that prohibits you from being a member of any other organization.");
+ }
+ }
+
+ ///
+ /// Private helper method to create a new organization.
+ /// This is common code used by both the cloud and self-hosted methods.
+ ///
+ private async Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)>
+ SignUpAsync(Organization organization,
+ Guid ownerId, string ownerKey, string? collectionName, bool withPayment)
+ {
+ try
+ {
+ await _organizationRepository.CreateAsync(organization);
+ await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
+ {
+ OrganizationId = organization.Id,
+ ApiKey = CoreHelpers.SecureRandomString(30),
+ Type = OrganizationApiKeyType.Default,
+ RevisionDate = DateTime.UtcNow,
+ });
+ await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
+
+ // ownerId == default if the org is created by a provider - in this case it's created without an
+ // owner and the first owner is immediately invited afterwards
+ OrganizationUser? orgUser = null;
+ if (ownerId != default)
+ {
+ orgUser = new OrganizationUser
+ {
+ OrganizationId = organization.Id,
+ UserId = ownerId,
+ Key = ownerKey,
+ AccessSecretsManager = organization.UseSecretsManager,
+ Type = OrganizationUserType.Owner,
+ Status = OrganizationUserStatusType.Confirmed,
+ CreationDate = organization.CreationDate,
+ RevisionDate = organization.CreationDate
+ };
+ orgUser.SetNewId();
+
+ await _organizationUserRepository.CreateAsync(orgUser);
+
+ var devices = await GetUserDeviceIdsAsync(orgUser.UserId!.Value);
+ await _pushRegistrationService.AddUserRegistrationOrganizationAsync(devices,
+ organization.Id.ToString());
+ await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
+ }
+
+ Collection? defaultCollection = null;
+ if (!string.IsNullOrWhiteSpace(collectionName))
+ {
+ defaultCollection = new Collection
+ {
+ Name = collectionName,
+ OrganizationId = organization.Id,
+ CreationDate = organization.CreationDate,
+ RevisionDate = organization.CreationDate
+ };
+
+ // Give the owner Can Manage access over the default collection
+ List? defaultOwnerAccess = null;
+ if (orgUser != null)
+ {
+ defaultOwnerAccess =
+ [
+ new CollectionAccessSelection
+ {
+ Id = orgUser.Id,
+ HidePasswords = false,
+ ReadOnly = false,
+ Manage = true
+ }
+ ];
+ }
+
+ await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
+ }
+
+ return (organization, orgUser, defaultCollection);
+ }
+ catch
+ {
+ if (withPayment)
+ {
+ await _paymentService.CancelAndRecoverChargesAsync(organization);
+ }
+
+ if (organization.Id != default(Guid))
+ {
+ await _organizationRepository.DeleteAsync(organization);
+ await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
+ }
+
+ throw;
+ }
+ }
+
+ private async Task> GetUserDeviceIdsAsync(Guid userId)
+ {
+ var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
+ return devices
+ .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
+ .Select(d => d.Id.ToString());
+ }
+}
diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs
index 2fa6772c62..bec9507adf 100644
--- a/src/Core/AdminConsole/Services/IOrganizationService.cs
+++ b/src/Core/AdminConsole/Services/IOrganizationService.cs
@@ -4,7 +4,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.Auth.Enums;
-using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
@@ -21,11 +20,6 @@ public interface IOrganizationService
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd);
Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
- ///
- /// Create a new organization on a self-hosted instance
- ///
- Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner,
- string ownerKey, string collectionName, string publicKey, string privateKey);
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);
Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated);
Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
index 3e81494fc3..4f25f5fc53 100644
--- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
+++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
@@ -20,7 +20,6 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
-using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
@@ -396,155 +395,6 @@ public class OrganizationService : IOrganizationService
}
}
- private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
- {
- var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
- if (anySingleOrgPolicies)
- {
- throw new BadRequestException("You may not create an organization. You belong to an organization " +
- "which has a policy that prohibits you from being a member of any other organization.");
- }
- }
-
- ///
- /// Create a new organization on a self-hosted instance
- ///
- public async Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(
- OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey,
- string privateKey)
- {
- if (license.LicenseType != LicenseType.Organization)
- {
- throw new BadRequestException("Premium licenses cannot be applied to an organization. " +
- "Upload this license from your personal account settings page.");
- }
-
- var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);
- var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception);
-
- if (!canUse)
- {
- throw new BadRequestException(exception);
- }
-
- var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();
- if (enabledOrgs.Any(o => string.Equals(o.LicenseKey, license.LicenseKey)))
- {
- throw new BadRequestException("License is already in use by another organization.");
- }
-
- await ValidateSignUpPoliciesAsync(owner.Id);
-
- var organization = claimsPrincipal != null
- // If the ClaimsPrincipal exists (there's a token on the license), use it to build the organization.
- ? OrganizationFactory.Create(owner, claimsPrincipal, publicKey, privateKey)
- // If there's no ClaimsPrincipal (there's no token on the license), use the license to build the organization.
- : OrganizationFactory.Create(owner, license, publicKey, privateKey);
-
- var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
-
- var dir = $"{_globalSettings.LicenseDirectory}/organization";
- Directory.CreateDirectory(dir);
- await using var fs = new FileStream(Path.Combine(dir, $"{organization.Id}.json"), FileMode.Create);
- await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);
- return (result.organization, result.organizationUser);
- }
-
- ///
- /// Private helper method to create a new organization.
- /// This is common code used by both the cloud and self-hosted methods.
- ///
- private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)>
- SignUpAsync(Organization organization,
- Guid ownerId, string ownerKey, string collectionName, bool withPayment)
- {
- try
- {
- await _organizationRepository.CreateAsync(organization);
- await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
- {
- OrganizationId = organization.Id,
- ApiKey = CoreHelpers.SecureRandomString(30),
- Type = OrganizationApiKeyType.Default,
- RevisionDate = DateTime.UtcNow,
- });
- await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
-
- // ownerId == default if the org is created by a provider - in this case it's created without an
- // owner and the first owner is immediately invited afterwards
- OrganizationUser orgUser = null;
- if (ownerId != default)
- {
- orgUser = new OrganizationUser
- {
- OrganizationId = organization.Id,
- UserId = ownerId,
- Key = ownerKey,
- AccessSecretsManager = organization.UseSecretsManager,
- Type = OrganizationUserType.Owner,
- Status = OrganizationUserStatusType.Confirmed,
- CreationDate = organization.CreationDate,
- RevisionDate = organization.CreationDate
- };
- orgUser.SetNewId();
-
- await _organizationUserRepository.CreateAsync(orgUser);
-
- var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
- await _pushRegistrationService.AddUserRegistrationOrganizationAsync(devices,
- organization.Id.ToString());
- await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
- }
-
- Collection defaultCollection = null;
- if (!string.IsNullOrWhiteSpace(collectionName))
- {
- defaultCollection = new Collection
- {
- Name = collectionName,
- OrganizationId = organization.Id,
- CreationDate = organization.CreationDate,
- RevisionDate = organization.CreationDate
- };
-
- // Give the owner Can Manage access over the default collection
- List defaultOwnerAccess = null;
- if (orgUser != null)
- {
- defaultOwnerAccess =
- [
- new CollectionAccessSelection
- {
- Id = orgUser.Id,
- HidePasswords = false,
- ReadOnly = false,
- Manage = true
- }
- ];
- }
-
- await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
- }
-
- return (organization, orgUser, defaultCollection);
- }
- catch
- {
- if (withPayment)
- {
- await _paymentService.CancelAndRecoverChargesAsync(organization);
- }
-
- if (organization.Id != default(Guid))
- {
- await _organizationRepository.DeleteAsync(organization);
- await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
- }
-
- throw;
- }
- }
-
public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate)
{
var org = await GetOrgById(organizationId);
@@ -1338,14 +1188,6 @@ public class OrganizationService : IOrganizationService
await _groupRepository.UpdateUsersAsync(group.Id, users);
}
- private async Task> GetUserDeviceIdsAsync(Guid userId)
- {
- var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
- return devices
- .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
- .Select(d => d.Id.ToString());
- }
-
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
{
await _organizationRepository.ReplaceAsync(org);
diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
index ae24017e48..b78a305d31 100644
--- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
+++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
@@ -71,6 +71,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
}
private static void AddOrganizationDeleteCommands(this IServiceCollection services)
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommandTests.cs
new file mode 100644
index 0000000000..26c092797b
--- /dev/null
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommandTests.cs
@@ -0,0 +1,351 @@
+using System.Security.Claims;
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
+using Bit.Core.AdminConsole.Services;
+using Bit.Core.Billing.Organizations.Models;
+using Bit.Core.Billing.Services;
+using Bit.Core.Entities;
+using Bit.Core.Enums;
+using Bit.Core.Exceptions;
+using Bit.Core.Models.Data;
+using Bit.Core.Platform.Push;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
+using Bit.Core.Settings;
+using Bit.Test.Common.AutoFixture;
+using Bit.Test.Common.AutoFixture.Attributes;
+using NSubstitute;
+using NSubstitute.ExceptionExtensions;
+using Xunit;
+
+namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
+
+[SutProviderCustomize]
+public class SelfHostedOrganizationSignUpCommandTests
+{
+ [Theory, BitAutoData]
+ public async Task SignUpAsync_WithValidRequest_CreatesOrganizationSuccessfully(
+ User owner, string ownerKey, string collectionName, string publicKey,
+ string privateKey, List devices,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ var globalSettings = sutProvider.GetDependency();
+ var license = CreateValidOrganizationLicense(globalSettings);
+
+ SetupCommonMocks(sutProvider, owner);
+ SetupLicenseValidation(sutProvider, license);
+
+ sutProvider.GetDependency()
+ .GetManyByUserIdAsync(owner.Id)
+ .Returns(devices);
+
+ // Act
+ var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey);
+
+ // Assert
+ Assert.NotNull(result.organization);
+ Assert.NotNull(result.organizationUser);
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(result.organization);
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(Arg.Is(key =>
+ key.OrganizationId == result.organization.Id &&
+ key.Type == OrganizationApiKeyType.Default &&
+ !string.IsNullOrEmpty(key.ApiKey)));
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .UpsertOrganizationAbilityAsync(result.organization);
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(Arg.Is(user =>
+ user.OrganizationId == result.organization.Id &&
+ user.UserId == owner.Id &&
+ user.Key == ownerKey &&
+ user.Type == OrganizationUserType.Owner &&
+ user.Status == OrganizationUserStatusType.Confirmed));
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(
+ Arg.Is(c => c.Name == collectionName && c.OrganizationId == result.organization.Id),
+ Arg.Is>(groups => groups == null),
+ Arg.Is>(access =>
+ access.Any(a => a.Id == result.organizationUser.Id && a.Manage && !a.ReadOnly && !a.HidePasswords)));
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .PushSyncOrgKeysAsync(owner.Id);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SignUpAsync_WithPremiumLicense_ThrowsBadRequestException(
+ User owner, string ownerKey, string collectionName,
+ string publicKey, string privateKey,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ var globalSettings = sutProvider.GetDependency();
+ var license = CreateValidOrganizationLicense(globalSettings, LicenseType.User);
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));
+
+ Assert.Contains("Premium licenses cannot be applied to an organization", exception.Message);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SignUpAsync_WithInvalidLicense_ThrowsBadRequestException(
+ User owner, string ownerKey, string collectionName,
+ string publicKey, string privateKey,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ var globalSettings = sutProvider.GetDependency();
+ var license = CreateValidOrganizationLicense(globalSettings);
+ license.CanUse(globalSettings, sutProvider.GetDependency(), null, out _)
+ .Returns(false);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));
+ }
+
+ [Theory, BitAutoData]
+ public async Task SignUpAsync_WithLicenseAlreadyInUse_ThrowsBadRequestException(
+ User owner, string ownerKey, string collectionName,
+ string publicKey, string privateKey, Organization existingOrganization,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ var globalSettings = sutProvider.GetDependency();
+ var license = CreateValidOrganizationLicense(globalSettings);
+ existingOrganization.LicenseKey = license.LicenseKey;
+
+ SetupCommonMocks(sutProvider, owner);
+ SetupLicenseValidation(sutProvider, license);
+
+ sutProvider.GetDependency()
+ .GetManyByEnabledAsync()
+ .Returns(new List { existingOrganization });
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));
+
+ Assert.Contains("License is already in use", exception.Message);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SignUpAsync_WithSingleOrgPolicy_ThrowsBadRequestException(
+ User owner, string ownerKey, string collectionName,
+ string publicKey, string privateKey,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ var globalSettings = sutProvider.GetDependency();
+ var license = CreateValidOrganizationLicense(globalSettings);
+
+ SetupCommonMocks(sutProvider, owner);
+ SetupLicenseValidation(sutProvider, license);
+
+ sutProvider.GetDependency()
+ .AnyPoliciesApplicableToUserAsync(owner.Id, PolicyType.SingleOrg)
+ .Returns(true);
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));
+
+ Assert.Contains("You may not create an organization", exception.Message);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SignUpAsync_WithClaimsPrincipal_UsesClaimsPrincipalToCreateOrganization(
+ User owner, string ownerKey, string collectionName,
+ string publicKey, string privateKey, ClaimsPrincipal claimsPrincipal,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ var globalSettings = sutProvider.GetDependency();
+ var license = CreateValidOrganizationLicense(globalSettings);
+
+ SetupCommonMocks(sutProvider, owner);
+ SetupLicenseValidation(sutProvider, license);
+
+ sutProvider.GetDependency()
+ .GetClaimsPrincipalFromLicense(license)
+ .Returns(claimsPrincipal);
+
+ sutProvider.GetDependency()
+ .GetManyByUserIdAsync(owner.Id)
+ .Returns(new List());
+
+ // Act
+ var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey);
+
+ // Assert
+ Assert.NotNull(result.organization);
+ Assert.NotNull(result.organizationUser);
+
+ sutProvider.GetDependency()
+ .Received(1)
+ .GetClaimsPrincipalFromLicense(license);
+ }
+
+ [Theory, BitAutoData]
+ public async Task SignUpAsync_WithoutCollectionName_DoesNotCreateCollection(
+ User owner, string ownerKey, string publicKey, string privateKey,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ var globalSettings = sutProvider.GetDependency();
+ var license = CreateValidOrganizationLicense(globalSettings);
+
+ SetupCommonMocks(sutProvider, owner);
+ SetupLicenseValidation(sutProvider, license);
+
+ sutProvider.GetDependency()
+ .GetManyByUserIdAsync(owner.Id)
+ .Returns(new List());
+
+ // Act
+ var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, null, publicKey, privateKey);
+
+ // Assert
+ Assert.NotNull(result.organization);
+ Assert.NotNull(result.organizationUser);
+
+ await sutProvider.GetDependency()
+ .DidNotReceive()
+ .CreateAsync(Arg.Any(), Arg.Is>(x => true), Arg.Is>(x => true));
+ }
+
+ [Theory, BitAutoData]
+ public async Task SignUpAsync_WithDevices_RegistersDevicesForPushNotifications(
+ User owner, string ownerKey, string collectionName,
+ string publicKey, string privateKey, List devices,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ var globalSettings = sutProvider.GetDependency();
+ var license = CreateValidOrganizationLicense(globalSettings);
+
+ foreach (var device in devices)
+ {
+ device.PushToken = "push-token-" + device.Id;
+ }
+
+ SetupCommonMocks(sutProvider, owner);
+ SetupLicenseValidation(sutProvider, license);
+
+ sutProvider.GetDependency()
+ .GetManyByUserIdAsync(owner.Id)
+ .Returns(devices);
+
+ // Act
+ var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey);
+
+ // Assert
+ Assert.NotNull(result.organization);
+ Assert.NotNull(result.organizationUser);
+
+ var expectedDeviceIds = devices.Select(d => d.Id.ToString());
+ await sutProvider.GetDependency()
+ .Received(1)
+ .AddUserRegistrationOrganizationAsync(
+ Arg.Is>(ids => ids.SequenceEqual(expectedDeviceIds)),
+ result.organization.Id.ToString());
+ }
+
+ [Theory, BitAutoData]
+ public async Task SignUpAsync_OnException_CleansUpOrganization(
+ User owner, string ownerKey, string collectionName,
+ string publicKey, string privateKey,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ var globalSettings = sutProvider.GetDependency();
+ var license = CreateValidOrganizationLicense(globalSettings);
+
+ SetupCommonMocks(sutProvider, owner);
+ SetupLicenseValidation(sutProvider, license);
+
+ sutProvider.GetDependency()
+ .CreateAsync(Arg.Any())
+ .Throws(new Exception("Test exception"));
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .DeleteAsync(Arg.Any());
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .DeleteOrganizationAbilityAsync(Arg.Any());
+ }
+
+ private void SetupCommonMocks(
+ SutProvider sutProvider,
+ User owner)
+ {
+ var globalSettings = sutProvider.GetDependency();
+
+ sutProvider.GetDependency()
+ .CreateAsync(Arg.Any())
+ .Returns(callInfo =>
+ {
+ var org = callInfo.Arg();
+ org.Id = Guid.NewGuid();
+ return Task.FromResult(org);
+ });
+
+ sutProvider.GetDependency()
+ .AnyPoliciesApplicableToUserAsync(owner.Id, PolicyType.SingleOrg)
+ .Returns(false);
+
+ globalSettings.LicenseDirectory.Returns("/tmp/licenses");
+ }
+
+ private void SetupLicenseValidation(
+ SutProvider sutProvider,
+ OrganizationLicense license)
+ {
+ var globalSettings = sutProvider.GetDependency();
+
+ sutProvider.GetDependency()
+ .VerifyLicense(license)
+ .Returns(true);
+
+ license.CanUse(globalSettings, sutProvider.GetDependency(), null, out _)
+ .Returns(true);
+ }
+
+ private OrganizationLicense CreateValidOrganizationLicense(
+ IGlobalSettings globalSettings,
+ LicenseType licenseType = LicenseType.Organization)
+ {
+ return new OrganizationLicense
+ {
+ LicenseType = licenseType,
+ Signature = Guid.NewGuid().ToString().Replace('-', '+'),
+ Issued = DateTime.UtcNow.AddDays(-1),
+ Expires = DateTime.UtcNow.AddDays(10),
+ Version = OrganizationLicense.CurrentLicenseFileVersion,
+ InstallationId = globalSettings.Installation.Id,
+ Enabled = true,
+ SelfHost = true
+ };
+ }
+}