mirror of
https://github.com/bitwarden/server
synced 2025-12-30 23:23:37 +00:00
Merge branch 'main' into SM-1571-DisableSMAdsForUsers
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using AutoFixture;
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
@@ -23,6 +25,7 @@ public class CurrentContextOrganizationCustomization : ICustomization
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class CurrentContextOrganizationCustomizeAttribute : BitCustomizeAttribute
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
@@ -38,3 +41,19 @@ public class CurrentContextOrganizationCustomizeAttribute : BitCustomizeAttribut
|
||||
AccessSecretsManager = AccessSecretsManager
|
||||
};
|
||||
}
|
||||
|
||||
public class CurrentContextOrganizationAttribute : CustomizeAttribute
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public OrganizationUserType Type { get; set; } = OrganizationUserType.User;
|
||||
public Permissions Permissions { get; set; } = new();
|
||||
public bool AccessSecretsManager { get; set; } = false;
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo _) => new CurrentContextOrganizationCustomization
|
||||
{
|
||||
Id = Id,
|
||||
Type = Type,
|
||||
Permissions = Permissions,
|
||||
AccessSecretsManager = AccessSecretsManager
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AdminRecoverAccountCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RecoverAccountAsync_Success(
|
||||
string newMasterPassword,
|
||||
string key,
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser,
|
||||
User user,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupValidOrganization(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization);
|
||||
SetupValidOrganizationUser(organizationUser, organization.Id);
|
||||
SetupValidUser(sutProvider, user, organizationUser);
|
||||
SetupSuccessfulPasswordUpdate(sutProvider, user, newMasterPassword);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RecoverAccountAsync(organization.Id, organizationUser, newMasterPassword, key);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
await AssertSuccessAsync(sutProvider, user, key, organization, organizationUser);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RecoverAccountAsync_OrganizationDoesNotExist_ThrowsBadRequest(
|
||||
[OrganizationUser] OrganizationUser organizationUser,
|
||||
string newMasterPassword,
|
||||
string key,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var orgId = Guid.NewGuid();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(orgId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RecoverAccountAsync(orgId, organizationUser, newMasterPassword, key));
|
||||
Assert.Equal("Organization does not allow password reset.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RecoverAccountAsync_OrganizationDoesNotAllowResetPassword_ThrowsBadRequest(
|
||||
string newMasterPassword,
|
||||
string key,
|
||||
Organization organization,
|
||||
[OrganizationUser] OrganizationUser organizationUser,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.UseResetPassword = false;
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RecoverAccountAsync(organization.Id, organizationUser, newMasterPassword, key));
|
||||
Assert.Equal("Organization does not allow password reset.", exception.Message);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> InvalidPolicies => new object[][]
|
||||
{
|
||||
[new Policy { Type = PolicyType.ResetPassword, Enabled = false }], [null]
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(InvalidPolicies))]
|
||||
public async Task RecoverAccountAsync_InvalidPolicy_ThrowsBadRequest(
|
||||
Policy resetPasswordPolicy,
|
||||
string newMasterPassword,
|
||||
string key,
|
||||
Organization organization,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupValidOrganization(sutProvider, organization);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword)
|
||||
.Returns(resetPasswordPolicy);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RecoverAccountAsync(organization.Id, new OrganizationUser { Id = Guid.NewGuid() },
|
||||
newMasterPassword, key));
|
||||
Assert.Equal("Organization does not have the password reset policy enabled.", exception.Message);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> InvalidOrganizationUsers()
|
||||
{
|
||||
// Make an organization so we can use its Id
|
||||
var organization = new Fixture().Create<Organization>();
|
||||
|
||||
var nonConfirmed = new OrganizationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = organization.Id,
|
||||
Status = OrganizationUserStatusType.Invited
|
||||
};
|
||||
yield return [nonConfirmed, organization];
|
||||
|
||||
var wrongOrganization = new OrganizationUser
|
||||
{
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationId = Guid.NewGuid(), // Different org
|
||||
ResetPasswordKey = "test-key",
|
||||
UserId = Guid.NewGuid(),
|
||||
};
|
||||
yield return [wrongOrganization, organization];
|
||||
|
||||
var nullResetPasswordKey = new OrganizationUser
|
||||
{
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationId = organization.Id,
|
||||
ResetPasswordKey = null,
|
||||
UserId = Guid.NewGuid(),
|
||||
};
|
||||
yield return [nullResetPasswordKey, organization];
|
||||
|
||||
var emptyResetPasswordKey = new OrganizationUser
|
||||
{
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationId = organization.Id,
|
||||
ResetPasswordKey = "",
|
||||
UserId = Guid.NewGuid(),
|
||||
};
|
||||
yield return [emptyResetPasswordKey, organization];
|
||||
|
||||
var nullUserId = new OrganizationUser
|
||||
{
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationId = organization.Id,
|
||||
ResetPasswordKey = "test-key",
|
||||
UserId = null,
|
||||
};
|
||||
yield return [nullUserId, organization];
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(InvalidOrganizationUsers))]
|
||||
public async Task RecoverAccountAsync_OrganizationUserIsInvalid_ThrowsBadRequest(
|
||||
OrganizationUser organizationUser,
|
||||
Organization organization,
|
||||
string newMasterPassword,
|
||||
string key,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupValidOrganization(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RecoverAccountAsync(organization.Id, organizationUser, newMasterPassword, key));
|
||||
Assert.Equal("Organization User not valid", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RecoverAccountAsync_UserDoesNotExist_ThrowsNotFoundException(
|
||||
string newMasterPassword,
|
||||
string key,
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupValidOrganization(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization);
|
||||
SetupValidOrganizationUser(organizationUser, organization.Id);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(organizationUser.UserId!.Value)
|
||||
.Returns((User)null);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||
sutProvider.Sut.RecoverAccountAsync(organization.Id, organizationUser, newMasterPassword, key));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RecoverAccountAsync_UserUsesKeyConnector_ThrowsBadRequest(
|
||||
string newMasterPassword,
|
||||
string key,
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser,
|
||||
User user,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupValidOrganization(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization);
|
||||
SetupValidOrganizationUser(organizationUser, organization.Id);
|
||||
user.UsesKeyConnector = true;
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(organizationUser.UserId!.Value)
|
||||
.Returns(user);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RecoverAccountAsync(organization.Id, organizationUser, newMasterPassword, key));
|
||||
Assert.Equal("Cannot reset password of a user with Key Connector.", exception.Message);
|
||||
}
|
||||
|
||||
private static void SetupValidOrganization(SutProvider<AdminRecoverAccountCommand> sutProvider, Organization organization)
|
||||
{
|
||||
organization.UseResetPassword = true;
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
}
|
||||
|
||||
private static void SetupValidPolicy(SutProvider<AdminRecoverAccountCommand> sutProvider, Organization organization)
|
||||
{
|
||||
var policy = new Policy { Type = PolicyType.ResetPassword, Enabled = true };
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword)
|
||||
.Returns(policy);
|
||||
}
|
||||
|
||||
private static void SetupValidOrganizationUser(OrganizationUser organizationUser, Guid orgId)
|
||||
{
|
||||
organizationUser.Status = OrganizationUserStatusType.Confirmed;
|
||||
organizationUser.OrganizationId = orgId;
|
||||
organizationUser.ResetPasswordKey = "test-key";
|
||||
organizationUser.Type = OrganizationUserType.User;
|
||||
}
|
||||
|
||||
private static void SetupValidUser(SutProvider<AdminRecoverAccountCommand> sutProvider, User user, OrganizationUser organizationUser)
|
||||
{
|
||||
user.Id = organizationUser.UserId!.Value;
|
||||
user.UsesKeyConnector = false;
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
}
|
||||
|
||||
private static void SetupSuccessfulPasswordUpdate(SutProvider<AdminRecoverAccountCommand> sutProvider, User user, string newMasterPassword)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.UpdatePasswordHash(user, newMasterPassword)
|
||||
.Returns(IdentityResult.Success);
|
||||
}
|
||||
|
||||
private static async Task AssertSuccessAsync(SutProvider<AdminRecoverAccountCommand> sutProvider, User user, string key,
|
||||
Organization organization, OrganizationUser organizationUser)
|
||||
{
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(
|
||||
Arg.Is<User>(u =>
|
||||
u.Id == user.Id &&
|
||||
u.Key == key &&
|
||||
u.ForcePasswordReset == true &&
|
||||
u.RevisionDate == u.AccountRevisionDate &&
|
||||
u.LastPasswordChangeDate == u.RevisionDate));
|
||||
|
||||
await sutProvider.GetDependency<IMailService>().Received(1).SendAdminResetPasswordEmailAsync(
|
||||
Arg.Is(user.Email),
|
||||
Arg.Is(user.Name),
|
||||
Arg.Is(organization.DisplayName()));
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(
|
||||
Arg.Is(organizationUser),
|
||||
Arg.Is(EventType.OrganizationUser_AdminResetPassword));
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushLogOutAsync(
|
||||
Arg.Is(user.Id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
|
||||
public class UriMatchDefaultPolicyValidatorTests
|
||||
{
|
||||
private readonly UriMatchDefaultPolicyValidator _validator = new();
|
||||
|
||||
[Fact]
|
||||
// Test that the Type property returns the correct PolicyType for this validator
|
||||
public void Type_ReturnsUriMatchDefaults()
|
||||
{
|
||||
Assert.Equal(PolicyType.UriMatchDefaults, _validator.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// Test that the RequiredPolicies property returns exactly one policy (SingleOrg) as a prerequisite
|
||||
// for enabling the UriMatchDefaults policy, ensuring proper policy dependency enforcement
|
||||
public void RequiredPolicies_ReturnsSingleOrgPolicy()
|
||||
{
|
||||
var requiredPolicies = _validator.RequiredPolicies.ToList();
|
||||
|
||||
Assert.Single(requiredPolicies);
|
||||
Assert.Contains(PolicyType.SingleOrg, requiredPolicies);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Utilities;
|
||||
|
||||
public class PolicyDataValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_NullData_ReturnsNull()
|
||||
{
|
||||
var result = PolicyDataValidator.ValidateAndSerialize(null, PolicyType.MasterPassword);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_ValidData_ReturnsSerializedJson()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minLength", 12 } };
|
||||
|
||||
var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("\"minLength\":12", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_InvalidDataType_ThrowsBadRequestException()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minLength", "not a number" } };
|
||||
|
||||
var exception = Assert.Throws<BadRequestException>(() =>
|
||||
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
|
||||
|
||||
Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
|
||||
Assert.Contains("minLength", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndDeserializeMetadata_NullMetadata_ReturnsEmptyMetadataModel()
|
||||
{
|
||||
var result = PolicyDataValidator.ValidateAndDeserializeMetadata(null, PolicyType.SingleOrg);
|
||||
|
||||
Assert.IsType<EmptyMetadataModel>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndDeserializeMetadata_ValidMetadata_ReturnsModel()
|
||||
{
|
||||
var metadata = new Dictionary<string, object> { { "defaultUserCollectionName", "collection name" } };
|
||||
|
||||
var result = PolicyDataValidator.ValidateAndDeserializeMetadata(metadata, PolicyType.OrganizationDataOwnership);
|
||||
|
||||
Assert.IsType<OrganizationModelOwnershipPolicyModel>(result);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
@@ -34,6 +36,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
private readonly IUserService _userService = Substitute.For<IUserService>();
|
||||
private readonly IPushNotificationService _pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||
private readonly IHasPaymentMethodQuery _hasPaymentMethodQuery = Substitute.For<IHasPaymentMethodQuery>();
|
||||
private readonly IUpdatePaymentMethodCommand _updatePaymentMethodCommand = Substitute.For<IUpdatePaymentMethodCommand>();
|
||||
private readonly CreatePremiumCloudHostedSubscriptionCommand _command;
|
||||
|
||||
public CreatePremiumCloudHostedSubscriptionCommandTests()
|
||||
@@ -62,7 +66,9 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
_userService,
|
||||
_pushNotificationService,
|
||||
Substitute.For<ILogger<CreatePremiumCloudHostedSubscriptionCommand>>(),
|
||||
_pricingClient);
|
||||
_pricingClient,
|
||||
_hasPaymentMethodQuery,
|
||||
_updatePaymentMethodCommand);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -314,7 +320,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_UserHasExistingGatewayCustomerId_UsesExistingCustomer(
|
||||
public async Task Run_UserHasExistingGatewayCustomerIdAndPaymentMethod_UsesExistingCustomer(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
@@ -347,6 +353,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
// Mock that the user has a payment method (this is the key difference from the credit purchase case)
|
||||
_hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true);
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
@@ -358,6 +366,75 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
Assert.True(result.IsT0);
|
||||
await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());
|
||||
await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
|
||||
await _updatePaymentMethodCommand.DidNotReceive().Run(Arg.Any<User>(), Arg.Any<TokenizedPaymentMethod>(), Arg.Any<BillingAddress>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_UserPreviouslyPurchasedCreditWithoutPaymentMethod_UpdatesPaymentMethodAndCreatesSubscription(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = "existing_customer_123"; // Customer exists from previous credit purchase
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.Card;
|
||||
paymentMethod.Token = "card_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "existing_customer_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active";
|
||||
mockSubscription.Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
MaskedPaymentMethod mockMaskedPaymentMethod = new MaskedCard
|
||||
{
|
||||
Brand = "visa",
|
||||
Last4 = "1234",
|
||||
Expiration = "12/2025"
|
||||
};
|
||||
|
||||
// Mock that the user does NOT have a payment method (simulating credit purchase scenario)
|
||||
_hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(false);
|
||||
_updatePaymentMethodCommand.Run(Arg.Any<User>(), Arg.Any<TokenizedPaymentMethod>(), Arg.Any<BillingAddress>())
|
||||
.Returns(mockMaskedPaymentMethod);
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
// Verify that update payment method was called (new behavior for credit purchase case)
|
||||
await _updatePaymentMethodCommand.Received(1).Run(user, paymentMethod, billingAddress);
|
||||
// Verify GetCustomerOrThrow was called after updating payment method
|
||||
await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());
|
||||
// Verify no new customer was created
|
||||
await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
|
||||
// Verify subscription was created
|
||||
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
// Verify user was updated correctly
|
||||
Assert.True(user.Premium);
|
||||
await _userService.Received(1).SaveUserAsync(user);
|
||||
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.Platform.Mailer;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Core.Test.Platform.Mailer.TestMail;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Platform.Mailer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Platform.Mail.Delivery;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Core.Test.Platform.Mailer.TestMail;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Mailer;
|
||||
|
||||
public class MailerTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task SendEmailAsync()
|
||||
{
|
||||
var deliveryService = Substitute.For<IMailDeliveryService>();
|
||||
var mailer = new Core.Platform.Mailer.Mailer(new HandlebarMailRenderer(), deliveryService);
|
||||
var mailer = new Core.Platform.Mail.Mailer.Mailer(new HandlebarMailRenderer(), deliveryService);
|
||||
|
||||
var mail = new TestMail.TestMail()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.Platform.Mailer;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Mailer.TestMail;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Amazon.SimpleEmail;
|
||||
using Amazon.SimpleEmail.Model;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Platform.Mail.Delivery;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -6,7 +6,10 @@ using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Business;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Platform.Mail.Delivery;
|
||||
using Bit.Core.Platform.Mail.Enqueuing;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Services.Mail;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Platform.Mail.Delivery;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Platform.Mail.Delivery;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
Reference in New Issue
Block a user