mirror of
https://github.com/bitwarden/server.git
synced 2025-04-25 06:42:22 -05:00
[PM-20230] - Send owners email when autoscaling (#5658)
* Added email when autoscaling. Added tests as well. * Wrote tests. Renamed methods.
This commit is contained in:
parent
4379e326a5
commit
89fc27b014
@ -206,10 +206,26 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
|
||||
private async Task SendAdditionalEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
{
|
||||
await SendPasswordManagerMaxSeatLimitEmailsAsync(validatedResult, organization);
|
||||
await NotifyOwnersIfAutoscaleOccursAsync(validatedResult, organization);
|
||||
await NotifyOwnersIfPasswordManagerMaxSeatLimitReachedAsync(validatedResult, organization);
|
||||
}
|
||||
|
||||
private async Task SendPasswordManagerMaxSeatLimitEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
private async Task NotifyOwnersIfAutoscaleOccursAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
{
|
||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0
|
||||
&& !organization.OwnersNotifiedOfAutoscaling.HasValue)
|
||||
{
|
||||
await mailService.SendOrganizationAutoscaledEmailAsync(
|
||||
organization,
|
||||
validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats!.Value,
|
||||
await GetOwnerEmailAddressesAsync(validatedResult.Value.InviteOrganization));
|
||||
|
||||
organization.OwnersNotifiedOfAutoscaling = validatedResult.Value.PerformedAt.UtcDateTime;
|
||||
await organizationRepository.UpsertAsync(organization);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifyOwnersIfPasswordManagerMaxSeatLimitReachedAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
{
|
||||
if (!validatedResult.Value.PasswordManagerSubscriptionUpdate.MaxSeatsReached)
|
||||
{
|
||||
|
@ -287,6 +287,77 @@ public class InviteOrganizationUserCommandTests
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task InviteScimOrganizationUserAsync_WhenValidInviteCausesOrgToAutoscale_ThenOrganizationOwnersShouldBeNotified(
|
||||
MailAddress address,
|
||||
Organization organization,
|
||||
OrganizationUser user,
|
||||
FakeTimeProvider timeProvider,
|
||||
string externalId,
|
||||
OrganizationUserUserDetails ownerDetails,
|
||||
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = address.Address;
|
||||
organization.Seats = 1;
|
||||
organization.MaxAutoscaleSeats = 2;
|
||||
organization.OwnersNotifiedOfAutoscaling = null;
|
||||
ownerDetails.Type = OrganizationUserType.Owner;
|
||||
|
||||
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites:
|
||||
[
|
||||
new OrganizationUserInvite(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
type: OrganizationUserType.User,
|
||||
permissions: new Permissions(),
|
||||
externalId: externalId,
|
||||
accessSecretsManager: true)
|
||||
],
|
||||
inviteOrganization: inviteOrganization,
|
||||
performedBy: Guid.Empty,
|
||||
timeProvider.GetUtcNow());
|
||||
|
||||
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
orgUserRepository
|
||||
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||
.Returns([]);
|
||||
orgUserRepository
|
||||
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||
.Returns([ownerDetails]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(
|
||||
GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||
.WithPasswordManagerUpdate(
|
||||
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||
|
||||
Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationAutoscaledEmailAsync(organization,
|
||||
inviteOrganization.Seats.Value,
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task InviteScimOrganizationUserAsync_WhenValidInviteIncreasesSeats_ThenSeatTotalShouldBeUpdated(
|
||||
@ -610,4 +681,237 @@ public class InviteOrganizationUserCommandTests
|
||||
.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2,
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == "provider@email.com")));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationIsManagedByAProviderAndAutoscaleOccurs_ThenAnEmailShouldBeSentToTheProvider(
|
||||
MailAddress address,
|
||||
Organization organization,
|
||||
OrganizationUser user,
|
||||
FakeTimeProvider timeProvider,
|
||||
string externalId,
|
||||
OrganizationUserUserDetails ownerDetails,
|
||||
ProviderOrganization providerOrganization,
|
||||
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = address.Address;
|
||||
organization.Seats = 1;
|
||||
organization.SmSeats = 1;
|
||||
organization.MaxAutoscaleSeats = 2;
|
||||
organization.MaxAutoscaleSmSeats = 2;
|
||||
organization.OwnersNotifiedOfAutoscaling = null;
|
||||
ownerDetails.Type = OrganizationUserType.Owner;
|
||||
|
||||
providerOrganization.OrganizationId = organization.Id;
|
||||
|
||||
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites: [
|
||||
new OrganizationUserInvite(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
type: OrganizationUserType.User,
|
||||
permissions: new Permissions(),
|
||||
externalId: externalId,
|
||||
accessSecretsManager: true)
|
||||
],
|
||||
inviteOrganization: inviteOrganization,
|
||||
performedBy: Guid.Empty,
|
||||
timeProvider.GetUtcNow());
|
||||
|
||||
var secretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, inviteOrganization.Plan, true)
|
||||
.AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager));
|
||||
|
||||
var passwordManagerSubscriptionUpdate =
|
||||
new PasswordManagerSubscriptionUpdate(inviteOrganization, 1, request.Invites.Length);
|
||||
|
||||
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
orgUserRepository
|
||||
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||
.Returns([]);
|
||||
orgUserRepository
|
||||
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||
.Returns([ownerDetails]);
|
||||
|
||||
var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
|
||||
orgRepository.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||
.WithPasswordManagerUpdate(passwordManagerSubscriptionUpdate)
|
||||
.WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate)));
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>()
|
||||
.GetByOrganizationId(organization.Id)
|
||||
.Returns(providerOrganization);
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyDetailsByProviderAsync(providerOrganization.ProviderId, ProviderUserStatusType.Confirmed)
|
||||
.Returns(new List<ProviderUserUserDetails>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Email = "provider@email.com"
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||
|
||||
sutProvider.GetDependency<IMailService>().Received(1)
|
||||
.SendOrganizationAutoscaledEmailAsync(organization, 1,
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == "provider@email.com")));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationAutoscalesButOwnersHaveAlreadyBeenNotified_ThenAnEmailShouldNotBeSent(
|
||||
MailAddress address,
|
||||
Organization organization,
|
||||
OrganizationUser user,
|
||||
FakeTimeProvider timeProvider,
|
||||
string externalId,
|
||||
OrganizationUserUserDetails ownerDetails,
|
||||
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = address.Address;
|
||||
organization.Seats = 1;
|
||||
organization.MaxAutoscaleSeats = 2;
|
||||
organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;
|
||||
ownerDetails.Type = OrganizationUserType.Owner;
|
||||
|
||||
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites:
|
||||
[
|
||||
new OrganizationUserInvite(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
type: OrganizationUserType.User,
|
||||
permissions: new Permissions(),
|
||||
externalId: externalId,
|
||||
accessSecretsManager: true)
|
||||
],
|
||||
inviteOrganization: inviteOrganization,
|
||||
performedBy: Guid.Empty,
|
||||
timeProvider.GetUtcNow());
|
||||
|
||||
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
orgUserRepository
|
||||
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||
.Returns([]);
|
||||
orgUserRepository
|
||||
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||
.Returns([ownerDetails]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(
|
||||
GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||
.WithPasswordManagerUpdate(
|
||||
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||
|
||||
Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendOrganizationAutoscaledEmailAsync(Arg.Any<Organization>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<IEnumerable<string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationDoesNotAutoScale_ThenAnEmailShouldNotBeSent(
|
||||
MailAddress address,
|
||||
Organization organization,
|
||||
OrganizationUser user,
|
||||
FakeTimeProvider timeProvider,
|
||||
string externalId,
|
||||
OrganizationUserUserDetails ownerDetails,
|
||||
SutProvider<InviteOrganizationUsersCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = address.Address;
|
||||
organization.Seats = 2;
|
||||
organization.MaxAutoscaleSeats = 2;
|
||||
organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;
|
||||
ownerDetails.Type = OrganizationUserType.Owner;
|
||||
|
||||
var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));
|
||||
|
||||
var request = new InviteOrganizationUsersRequest(
|
||||
invites:
|
||||
[
|
||||
new OrganizationUserInvite(
|
||||
email: user.Email,
|
||||
assignedCollections: [],
|
||||
groups: [],
|
||||
type: OrganizationUserType.User,
|
||||
permissions: new Permissions(),
|
||||
externalId: externalId,
|
||||
accessSecretsManager: true)
|
||||
],
|
||||
inviteOrganization: inviteOrganization,
|
||||
performedBy: Guid.Empty,
|
||||
timeProvider.GetUtcNow());
|
||||
|
||||
var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
|
||||
orgUserRepository
|
||||
.SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)
|
||||
.Returns([]);
|
||||
orgUserRepository
|
||||
.GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)
|
||||
.Returns([ownerDetails]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IInviteUsersValidator>()
|
||||
.ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())
|
||||
.Returns(new Valid<InviteOrganizationUsersValidationRequest>(
|
||||
GetInviteValidationRequestMock(request, inviteOrganization, organization)
|
||||
.WithPasswordManagerUpdate(
|
||||
new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||
|
||||
Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendOrganizationAutoscaledEmailAsync(Arg.Any<Organization>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<IEnumerable<string>>());
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user