mirror of
https://github.com/bitwarden/server.git
synced 2025-04-25 23:02:17 -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)
|
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)
|
if (!validatedResult.Value.PasswordManagerSubscriptionUpdate.MaxSeatsReached)
|
||||||
{
|
{
|
||||||
|
@ -287,6 +287,77 @@ public class InviteOrganizationUserCommandTests
|
|||||||
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));
|
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]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task InviteScimOrganizationUserAsync_WhenValidInviteIncreasesSeats_ThenSeatTotalShouldBeUpdated(
|
public async Task InviteScimOrganizationUserAsync_WhenValidInviteIncreasesSeats_ThenSeatTotalShouldBeUpdated(
|
||||||
@ -610,4 +681,237 @@ public class InviteOrganizationUserCommandTests
|
|||||||
.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2,
|
.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2,
|
||||||
Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == "provider@email.com")));
|
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