1
0
mirror of https://github.com/bitwarden/server synced 2025-12-10 21:33:41 +00:00

Merge branch 'main' into billing/PM-28128/stripe-bank-transfer-transaction-records

This commit is contained in:
Alex Morask
2025-12-05 10:46:22 -06:00
17 changed files with 413 additions and 107 deletions

View File

@@ -25,6 +25,12 @@
"connectionString": "UseDevelopmentStorage=true"
},
"developmentDirectory": "../../../dev",
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
"pricingUri": "https://billingpricing.qa.bitwarden.pw",
"mail": {
"smtp": {
"host": "localhost",
"port": 10250
}
}
}
}

View File

@@ -13,7 +13,11 @@
"mail": {
"sendGridApiKey": "SECRET",
"amazonConfigSetName": "Email",
"replyToEmail": "no-reply@bitwarden.com"
"replyToEmail": "no-reply@bitwarden.com",
"smtp": {
"host": "localhost",
"port": 10250
}
},
"identityServer": {
"certificateThumbprint": "SECRET"

View File

@@ -71,6 +71,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
private readonly IFeatureService _featureService;
private readonly IPricingClient _pricingClient;
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
private readonly IBulkResendOrganizationInvitesCommand _bulkResendOrganizationInvitesCommand;
private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
@@ -105,6 +106,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
IInitPendingOrganizationCommand initPendingOrganizationCommand,
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
IBulkResendOrganizationInvitesCommand bulkResendOrganizationInvitesCommand,
IAdminRecoverAccountCommand adminRecoverAccountCommand,
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand)
{
@@ -131,6 +133,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
_featureService = featureService;
_pricingClient = pricingClient;
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
_bulkResendOrganizationInvitesCommand = bulkResendOrganizationInvitesCommand;
_automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
@@ -273,7 +276,17 @@ public class OrganizationUsersController : BaseAdminConsoleController
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkReinvite(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
var userId = _userService.GetProperUserId(User);
var result = await _organizationService.ResendInvitesAsync(orgId, userId.Value, model.Ids);
IEnumerable<Tuple<Core.Entities.OrganizationUser, string>> result;
if (_featureService.IsEnabled(FeatureFlagKeys.IncreaseBulkReinviteLimitForCloud))
{
result = await _bulkResendOrganizationInvitesCommand.BulkResendInvitesAsync(orgId, userId.Value, model.Ids);
}
else
{
result = await _organizationService.ResendInvitesAsync(orgId, userId.Value, model.Ids);
}
return new ListResponseModel<OrganizationUserBulkResponseModel>(
result.Select(t => new OrganizationUserBulkResponseModel(t.Item1.Id, t.Item2)));
}

View File

@@ -0,0 +1,69 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.Utilities.DebuggingInstruments;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.Extensions.Logging;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
public class BulkResendOrganizationInvitesCommand : IBulkResendOrganizationInvitesCommand
{
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
private readonly ILogger<BulkResendOrganizationInvitesCommand> _logger;
public BulkResendOrganizationInvitesCommand(
IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
ILogger<BulkResendOrganizationInvitesCommand> logger)
{
_organizationUserRepository = organizationUserRepository;
_organizationRepository = organizationRepository;
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
_logger = logger;
}
public async Task<IEnumerable<Tuple<OrganizationUser, string>>> BulkResendInvitesAsync(
Guid organizationId,
Guid? invitingUserId,
IEnumerable<Guid> organizationUsersId)
{
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
_logger.LogUserInviteStateDiagnostics(orgUsers);
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null)
{
throw new NotFoundException();
}
var validUsers = new List<OrganizationUser>();
var result = new List<Tuple<OrganizationUser, string>>();
foreach (var orgUser in orgUsers)
{
if (orgUser.Status != OrganizationUserStatusType.Invited || orgUser.OrganizationId != organizationId)
{
result.Add(Tuple.Create(orgUser, "User invalid."));
}
else
{
validUsers.Add(orgUser);
}
}
if (validUsers.Any())
{
await _sendOrganizationInvitesCommand.SendInvitesAsync(
new SendInvitesRequest(validUsers, org));
result.AddRange(validUsers.Select(u => Tuple.Create(u, "")));
}
return result;
}
}

View File

@@ -0,0 +1,20 @@
using Bit.Core.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
public interface IBulkResendOrganizationInvitesCommand
{
/// <summary>
/// Resend invites to multiple organization users in bulk.
/// </summary>
/// <param name="organizationId">The ID of the organization.</param>
/// <param name="invitingUserId">The ID of the user who is resending the invites.</param>
/// <param name="organizationUsersId">The IDs of the organization users to resend invites to.</param>
/// <returns>A tuple containing the OrganizationUser and an error message (empty string if successful)</returns>
Task<IEnumerable<Tuple<OrganizationUser, string>>> BulkResendInvitesAsync(
Guid organizationId,
Guid? invitingUserId,
IEnumerable<Guid> organizationUsersId);
}

View File

@@ -5,6 +5,7 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
@@ -455,9 +456,7 @@ public class RegisterUserCommand : IRegisterUserCommand
else if (!string.IsNullOrEmpty(organization.DisplayName()))
{
// If the organization is Free or Families plan, send families welcome email
if (organization.PlanType is PlanType.FamiliesAnnually
or PlanType.FamiliesAnnually2019
or PlanType.Free)
if (organization.PlanType.GetProductTier() is ProductTierType.Free or ProductTierType.Families)
{
await _mailService.SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(user, organization.DisplayName());
}

View File

@@ -53,11 +53,37 @@
<style type="text/css">
@media only screen and (max-width:480px) {
.mj-bw-hero-responsive-img {
display: none !important;
}
}
@media only screen and (max-width:480px) {
.mj-bw-learn-more-footer-responsive-img {
display: none !important;
}
}
@media only screen and (max-width:479px) {
table.mj-full-width-mobile { width: 100% !important; }
td.mj-full-width-mobile { width: auto !important; }
}
@media only screen and (max-width:480px) {
.mj-bw-icon-row-text {
padding-left: 5px !important;
line-height: 20px;
}
.mj-bw-icon-row {
padding: 10px 15px;
width: fit-content !important;
}
}
</style>
<style type="text/css">
@@ -67,29 +93,8 @@
.border-fix > table > tbody > tr > td {
border-radius: 3px;
}
@media only screen and (max-width: 480px) {
.hide-small-img {
display: none !important;
}
.send-bubble {
padding-left: 20px;
padding-right: 20px;
width: 90% !important;
}
}
@media only screen and (max-width: 480px) {
.mj-bw-icon-row-text {
padding-left: 5px !important;
line-height: 20px;
}
.mj-bw-icon-row {
padding: 10px 15px;
width: fit-content !important;
}
}
</style>
<!-- Responsive icon visibility -->
<!-- Responsive styling for mj-bw-icon-row -->
</head>
<body style="word-spacing:normal;background-color:#e6e9ef;">
@@ -156,7 +161,7 @@
</h1>
<mj-text color="#fff" padding-top="0" padding-bottom="0">
<h2 style="font-weight: normal; font-size: 16px; line-height: 0px">
Let's get set up to autofill.
Lets get you set up to autofill.
</h2>
</mj-text></div>
@@ -176,7 +181,7 @@
<tbody>
<tr>
<td align="center" class="hide-small-img" style="font-size:0px;padding:0px;word-break:break-word;">
<td align="center" class="mj-bw-hero-responsive-img" style="font-size:0px;padding:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
@@ -256,7 +261,7 @@
<tr>
<td align="left" style="font-size:0px;padding:10px 15px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">A <b>{{OrganizationName}}</b> administrator will approve you
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">An administrator from <b>{{OrganizationName}}</b> will approve you
before you can share passwords. While you wait for approval, get
started with Bitwarden Password Manager:</div>
@@ -622,10 +627,10 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><p style="font-size: 18px; line-height: 28px; font-weight: bold;">
Learn more about Bitwarden
</p>
Find user guides, product documentation, and videos on the
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
Learn more about Bitwarden
</p>
Find user guides, product documentation, and videos on the
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
</td>
</tr>
@@ -643,7 +648,7 @@
<tbody>
<tr>
<td align="center" class="hide-small-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<td align="center" class="mj-bw-learn-more-footer-responsive-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>

View File

@@ -53,11 +53,37 @@
<style type="text/css">
@media only screen and (max-width:480px) {
.mj-bw-hero-responsive-img {
display: none !important;
}
}
@media only screen and (max-width:480px) {
.mj-bw-learn-more-footer-responsive-img {
display: none !important;
}
}
@media only screen and (max-width:479px) {
table.mj-full-width-mobile { width: 100% !important; }
td.mj-full-width-mobile { width: auto !important; }
}
@media only screen and (max-width:480px) {
.mj-bw-icon-row-text {
padding-left: 5px !important;
line-height: 20px;
}
.mj-bw-icon-row {
padding: 10px 15px;
width: fit-content !important;
}
}
</style>
<style type="text/css">
@@ -67,29 +93,8 @@
.border-fix > table > tbody > tr > td {
border-radius: 3px;
}
@media only screen and (max-width: 480px) {
.hide-small-img {
display: none !important;
}
.send-bubble {
padding-left: 20px;
padding-right: 20px;
width: 90% !important;
}
}
@media only screen and (max-width: 480px) {
.mj-bw-icon-row-text {
padding-left: 5px !important;
line-height: 20px;
}
.mj-bw-icon-row {
padding: 10px 15px;
width: fit-content !important;
}
}
</style>
<!-- Responsive icon visibility -->
<!-- Responsive styling for mj-bw-icon-row -->
</head>
<body style="word-spacing:normal;background-color:#e6e9ef;">
@@ -156,7 +161,7 @@
</h1>
<mj-text color="#fff" padding-top="0" padding-bottom="0">
<h2 style="font-weight: normal; font-size: 16px; line-height: 0px">
Let's get set up to autofill.
Lets get you set up to autofill.
</h2>
</mj-text></div>
@@ -176,7 +181,7 @@
<tbody>
<tr>
<td align="center" class="hide-small-img" style="font-size:0px;padding:0px;word-break:break-word;">
<td align="center" class="mj-bw-hero-responsive-img" style="font-size:0px;padding:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
@@ -621,10 +626,10 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><p style="font-size: 18px; line-height: 28px; font-weight: bold;">
Learn more about Bitwarden
</p>
Find user guides, product documentation, and videos on the
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
Learn more about Bitwarden
</p>
Find user guides, product documentation, and videos on the
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
</td>
</tr>
@@ -642,7 +647,7 @@
<tbody>
<tr>
<td align="center" class="hide-small-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<td align="center" class="mj-bw-learn-more-footer-responsive-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>

View File

@@ -53,11 +53,37 @@
<style type="text/css">
@media only screen and (max-width:480px) {
.mj-bw-hero-responsive-img {
display: none !important;
}
}
@media only screen and (max-width:480px) {
.mj-bw-learn-more-footer-responsive-img {
display: none !important;
}
}
@media only screen and (max-width:479px) {
table.mj-full-width-mobile { width: 100% !important; }
td.mj-full-width-mobile { width: auto !important; }
}
@media only screen and (max-width:480px) {
.mj-bw-icon-row-text {
padding-left: 5px !important;
line-height: 20px;
}
.mj-bw-icon-row {
padding: 10px 15px;
width: fit-content !important;
}
}
</style>
<style type="text/css">
@@ -67,29 +93,8 @@
.border-fix > table > tbody > tr > td {
border-radius: 3px;
}
@media only screen and (max-width: 480px) {
.hide-small-img {
display: none !important;
}
.send-bubble {
padding-left: 20px;
padding-right: 20px;
width: 90% !important;
}
}
@media only screen and (max-width: 480px) {
.mj-bw-icon-row-text {
padding-left: 5px !important;
line-height: 20px;
}
.mj-bw-icon-row {
padding: 10px 15px;
width: fit-content !important;
}
}
</style>
<!-- Responsive icon visibility -->
<!-- Responsive styling for mj-bw-icon-row -->
</head>
<body style="word-spacing:normal;background-color:#e6e9ef;">
@@ -156,7 +161,7 @@
</h1>
<mj-text color="#fff" padding-top="0" padding-bottom="0">
<h2 style="font-weight: normal; font-size: 16px; line-height: 0px">
Let's get set up to autofill.
Lets get you set up to autofill.
</h2>
</mj-text></div>
@@ -176,7 +181,7 @@
<tbody>
<tr>
<td align="center" class="hide-small-img" style="font-size:0px;padding:0px;word-break:break-word;">
<td align="center" class="mj-bw-hero-responsive-img" style="font-size:0px;padding:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
@@ -256,7 +261,7 @@
<tr>
<td align="left" style="font-size:0px;padding:10px 15px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">A <b>{{OrganizationName}}</b> administrator will need to confirm
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">An administrator from <b>{{OrganizationName}}</b> will need to confirm
you before you can share passwords. Get started with Bitwarden
Password Manager:</div>
@@ -622,10 +627,10 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><p style="font-size: 18px; line-height: 28px; font-weight: bold;">
Learn more about Bitwarden
</p>
Find user guides, product documentation, and videos on the
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
Learn more about Bitwarden
</p>
Find user guides, product documentation, and videos on the
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
</td>
</tr>
@@ -643,7 +648,7 @@
<tbody>
<tr>
<td align="center" class="hide-small-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<td align="center" class="mj-bw-learn-more-footer-responsive-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>

View File

@@ -18,16 +18,16 @@ class MjBwIconRow extends BodyComponent {
static defaultAttributes = {};
componentHeadStyle = (breakpoint) => {
headStyle = (breakpoint) => {
return `
@media only screen and (max-width:${breakpoint}): {
".mj-bw-icon-row-text": {
padding-left: "5px !important",
line-height: "20px",
},
".mj-bw-icon-row": {
padding: "10px 15px",
width: "fit-content !important",
@media only screen and (max-width:${breakpoint}) {
.mj-bw-icon-row-text {
padding-left: 5px !important;
line-height: 20px;
}
.mj-bw-icon-row {
padding: 10px 15px;
width: fit-content !important;
}
}
`;

View File

@@ -9,7 +9,7 @@
<mj-bw-hero
img-src="https://assets.bitwarden.com/email/v1/account-fill.png"
title="Welcome to Bitwarden!"
sub-title="Let's get set up to autofill."
sub-title="Lets get you set up to autofill."
/>
</mj-wrapper>

View File

@@ -9,7 +9,7 @@
<mj-bw-hero
img-src="https://assets.bitwarden.com/email/v1/account-fill.png"
title="Welcome to Bitwarden!"
sub-title="Let's get set up to autofill."
sub-title="Lets get you set up to autofill."
/>
</mj-wrapper>

View File

@@ -9,7 +9,7 @@
<mj-bw-hero
img-src="https://assets.bitwarden.com/email/v1/account-fill.png"
title="Welcome to Bitwarden!"
sub-title="Let's get set up to autofill."
sub-title="Lets get you set up to autofill."
/>
</mj-wrapper>

View File

@@ -197,6 +197,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IInviteOrganizationUsersCommand, InviteOrganizationUsersCommand>();
services.AddScoped<ISendOrganizationInvitesCommand, SendOrganizationInvitesCommand>();
services.AddScoped<IResendOrganizationInviteCommand, ResendOrganizationInviteCommand>();
services.AddScoped<IBulkResendOrganizationInvitesCommand, BulkResendOrganizationInvitesCommand>();
services.AddScoped<IInviteUsersValidator, InviteOrganizationUsersValidator>();
services.AddScoped<IInviteUsersOrganizationValidator, InviteUsersOrganizationValidator>();

View File

@@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
@@ -730,4 +731,68 @@ public class OrganizationUsersControllerTests
var problemResult = Assert.IsType<JsonHttpResult<ErrorResponseModel>>(result);
Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);
}
[Theory]
[BitAutoData]
public async Task BulkReinvite_WhenFeatureFlagEnabled_UsesBulkResendOrganizationInvitesCommand(
Guid organizationId,
OrganizationUserBulkRequestModel bulkRequestModel,
List<OrganizationUser> organizationUsers,
Guid userId,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.IncreaseBulkReinviteLimitForCloud)
.Returns(true);
var expectedResults = organizationUsers.Select(u => Tuple.Create(u, "")).ToList();
sutProvider.GetDependency<IBulkResendOrganizationInvitesCommand>()
.BulkResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids)
.Returns(expectedResults);
// Act
var response = await sutProvider.Sut.BulkReinvite(organizationId, bulkRequestModel);
// Assert
Assert.Equal(organizationUsers.Count, response.Data.Count());
await sutProvider.GetDependency<IBulkResendOrganizationInvitesCommand>()
.Received(1)
.BulkResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids);
}
[Theory]
[BitAutoData]
public async Task BulkReinvite_WhenFeatureFlagDisabled_UsesLegacyOrganizationService(
Guid organizationId,
OrganizationUserBulkRequestModel bulkRequestModel,
List<OrganizationUser> organizationUsers,
Guid userId,
SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.IncreaseBulkReinviteLimitForCloud)
.Returns(false);
var expectedResults = organizationUsers.Select(u => Tuple.Create(u, "")).ToList();
sutProvider.GetDependency<IOrganizationService>()
.ResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids)
.Returns(expectedResults);
// Act
var response = await sutProvider.Sut.BulkReinvite(organizationId, bulkRequestModel);
// Assert
Assert.Equal(organizationUsers.Count, response.Data.Count());
await sutProvider.GetDependency<IOrganizationService>()
.Received(1)
.ResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids);
}
}

View File

@@ -0,0 +1,113 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
[SutProviderCustomize]
public class BulkResendOrganizationInvitesCommandTests
{
[Theory]
[BitAutoData]
public async Task BulkResendInvitesAsync_ValidatesUsersAndSendsBatchInvite(
Organization organization,
OrganizationUser validUser1,
OrganizationUser validUser2,
OrganizationUser acceptedUser,
OrganizationUser wrongOrgUser,
SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)
{
validUser1.OrganizationId = organization.Id;
validUser1.Status = OrganizationUserStatusType.Invited;
validUser2.OrganizationId = organization.Id;
validUser2.Status = OrganizationUserStatusType.Invited;
acceptedUser.OrganizationId = organization.Id;
acceptedUser.Status = OrganizationUserStatusType.Accepted;
wrongOrgUser.OrganizationId = Guid.NewGuid();
wrongOrgUser.Status = OrganizationUserStatusType.Invited;
var users = new List<OrganizationUser> { validUser1, validUser2, acceptedUser, wrongOrgUser };
var userIds = users.Select(u => u.Id).ToList();
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(userIds).Returns(users);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var result = (await sutProvider.Sut.BulkResendInvitesAsync(organization.Id, null, userIds)).ToList();
Assert.Equal(4, result.Count);
Assert.Equal(2, result.Count(r => string.IsNullOrEmpty(r.Item2)));
Assert.Equal(2, result.Count(r => r.Item2 == "User invalid."));
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
.Received(1)
.SendInvitesAsync(Arg.Is<SendInvitesRequest>(req =>
req.Organization == organization &&
req.Users.Length == 2 &&
req.InitOrganization == false));
}
[Theory]
[BitAutoData]
public async Task BulkResendInvitesAsync_AllInvalidUsers_DoesNotSendInvites(
Organization organization,
List<OrganizationUser> organizationUsers,
SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)
{
foreach (var user in organizationUsers)
{
user.OrganizationId = organization.Id;
user.Status = OrganizationUserStatusType.Confirmed;
}
var userIds = organizationUsers.Select(u => u.Id).ToList();
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(userIds).Returns(organizationUsers);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var result = (await sutProvider.Sut.BulkResendInvitesAsync(organization.Id, null, userIds)).ToList();
Assert.Equal(organizationUsers.Count, result.Count);
Assert.All(result, r => Assert.Equal("User invalid.", r.Item2));
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().DidNotReceive()
.SendInvitesAsync(Arg.Any<SendInvitesRequest>());
}
[Theory]
[BitAutoData]
public async Task BulkResendInvitesAsync_OrganizationNotFound_ThrowsNotFoundException(
Guid organizationId,
List<Guid> userIds,
List<OrganizationUser> organizationUsers,
SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(userIds).Returns(organizationUsers);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns((Organization?)null);
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.BulkResendInvitesAsync(organizationId, null, userIds));
}
[Theory]
[BitAutoData]
public async Task BulkResendInvitesAsync_EmptyUserList_ReturnsEmpty(
Organization organization,
SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)
{
var emptyUserIds = new List<Guid>();
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(emptyUserIds).Returns(new List<OrganizationUser>());
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var result = await sutProvider.Sut.BulkResendInvitesAsync(organization.Id, null, emptyUserIds);
Assert.Empty(result);
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().DidNotReceive()
.SendInvitesAsync(Arg.Any<SendInvitesRequest>());
}
}

View File

@@ -1017,6 +1017,7 @@ public class RegisterUserCommandTests
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
[BitAutoData(PlanType.FamiliesAnnually2019)]
[BitAutoData(PlanType.FamiliesAnnually2025)]
[BitAutoData(PlanType.Free)]
public async Task SendWelcomeEmail_FamilyOrg_SendsFamilyWelcomeEmail(
PlanType planType,