mirror of
https://github.com/bitwarden/server
synced 2025-12-27 05:33:17 +00:00
Merge branch 'main' into SM-1571-DisableSMAdsForUsers
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Clients;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Entities;
|
||||
@@ -11,12 +12,18 @@ using Invoice = BitPayLight.Models.Invoice.Invoice;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Payment.Commands;
|
||||
|
||||
using static BitPayConstants;
|
||||
|
||||
public class CreateBitPayInvoiceForCreditCommandTests
|
||||
{
|
||||
private readonly IBitPayClient _bitPayClient = Substitute.For<IBitPayClient>();
|
||||
private readonly GlobalSettings _globalSettings = new()
|
||||
{
|
||||
BitPay = new GlobalSettings.BitPaySettings { NotificationUrl = "https://example.com/bitpay/notification" }
|
||||
BitPay = new GlobalSettings.BitPaySettings
|
||||
{
|
||||
NotificationUrl = "https://example.com/bitpay/notification",
|
||||
WebhookKey = "test-webhook-key"
|
||||
}
|
||||
};
|
||||
private const string _redirectUrl = "https://bitwarden.com/redirect";
|
||||
private readonly CreateBitPayInvoiceForCreditCommand _command;
|
||||
@@ -37,8 +44,8 @@ public class CreateBitPayInvoiceForCreditCommandTests
|
||||
_bitPayClient.CreateInvoice(Arg.Is<Invoice>(options =>
|
||||
options.Buyer.Email == user.Email &&
|
||||
options.Buyer.Name == user.Email &&
|
||||
options.NotificationUrl == _globalSettings.BitPay.NotificationUrl &&
|
||||
options.PosData == $"userId:{user.Id},accountCredit:1" &&
|
||||
options.NotificationUrl == $"{_globalSettings.BitPay.NotificationUrl}?key={_globalSettings.BitPay.WebhookKey}" &&
|
||||
options.PosData == $"userId:{user.Id},{PosDataKeys.AccountCredit}" &&
|
||||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
||||
options.Price == Convert.ToDouble(10M) &&
|
||||
options.RedirectUrl == _redirectUrl)).Returns(new Invoice { Url = "https://bitpay.com/invoice/123" });
|
||||
@@ -58,8 +65,8 @@ public class CreateBitPayInvoiceForCreditCommandTests
|
||||
_bitPayClient.CreateInvoice(Arg.Is<Invoice>(options =>
|
||||
options.Buyer.Email == organization.BillingEmail &&
|
||||
options.Buyer.Name == organization.Name &&
|
||||
options.NotificationUrl == _globalSettings.BitPay.NotificationUrl &&
|
||||
options.PosData == $"organizationId:{organization.Id},accountCredit:1" &&
|
||||
options.NotificationUrl == $"{_globalSettings.BitPay.NotificationUrl}?key={_globalSettings.BitPay.WebhookKey}" &&
|
||||
options.PosData == $"organizationId:{organization.Id},{PosDataKeys.AccountCredit}" &&
|
||||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
||||
options.Price == Convert.ToDouble(10M) &&
|
||||
options.RedirectUrl == _redirectUrl)).Returns(new Invoice { Url = "https://bitpay.com/invoice/123" });
|
||||
@@ -79,8 +86,8 @@ public class CreateBitPayInvoiceForCreditCommandTests
|
||||
_bitPayClient.CreateInvoice(Arg.Is<Invoice>(options =>
|
||||
options.Buyer.Email == provider.BillingEmail &&
|
||||
options.Buyer.Name == provider.Name &&
|
||||
options.NotificationUrl == _globalSettings.BitPay.NotificationUrl &&
|
||||
options.PosData == $"providerId:{provider.Id},accountCredit:1" &&
|
||||
options.NotificationUrl == $"{_globalSettings.BitPay.NotificationUrl}?key={_globalSettings.BitPay.WebhookKey}" &&
|
||||
options.PosData == $"providerId:{provider.Id},{PosDataKeys.AccountCredit}" &&
|
||||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
||||
options.Price == Convert.ToDouble(10M) &&
|
||||
options.RedirectUrl == _redirectUrl)).Returns(new Invoice { Url = "https://bitpay.com/invoice/123" });
|
||||
|
||||
112
test/Core.Test/Billing/Payment/Models/PaymentMethodTests.cs
Normal file
112
test/Core.Test/Billing/Payment/Models/PaymentMethodTests.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Payment.Models;
|
||||
|
||||
public class PaymentMethodTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("{\"cardNumber\":\"1234\"}")]
|
||||
[InlineData("{\"type\":\"unknown_type\",\"data\":\"value\"}")]
|
||||
[InlineData("{\"type\":\"invalid\",\"token\":\"test-token\"}")]
|
||||
[InlineData("{\"type\":\"invalid\"}")]
|
||||
public void Read_ShouldThrowJsonException_OnInvalidOrMissingType(string json)
|
||||
{
|
||||
// Arrange
|
||||
var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<PaymentMethod>(json, options));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"type\":\"card\"}")]
|
||||
[InlineData("{\"type\":\"card\",\"token\":\"\"}")]
|
||||
[InlineData("{\"type\":\"card\",\"token\":null}")]
|
||||
public void Read_ShouldThrowJsonException_OnInvalidTokenizedPaymentMethodToken(string json)
|
||||
{
|
||||
// Arrange
|
||||
var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<PaymentMethod>(json, options));
|
||||
}
|
||||
|
||||
// Tokenized payment method deserialization
|
||||
[Theory]
|
||||
[InlineData("bankAccount", TokenizablePaymentMethodType.BankAccount)]
|
||||
[InlineData("card", TokenizablePaymentMethodType.Card)]
|
||||
[InlineData("payPal", TokenizablePaymentMethodType.PayPal)]
|
||||
public void Read_ShouldDeserializeTokenizedPaymentMethods(string typeString, TokenizablePaymentMethodType expectedType)
|
||||
{
|
||||
// Arrange
|
||||
var json = $"{{\"type\":\"{typeString}\",\"token\":\"test-token\"}}";
|
||||
var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };
|
||||
|
||||
// Act
|
||||
var result = JsonSerializer.Deserialize<PaymentMethod>(json, options);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsTokenized);
|
||||
Assert.Equal(expectedType, result.AsT0.Type);
|
||||
Assert.Equal("test-token", result.AsT0.Token);
|
||||
}
|
||||
|
||||
// Non-tokenized payment method deserialization
|
||||
[Theory]
|
||||
[InlineData("accountcredit", NonTokenizablePaymentMethodType.AccountCredit)]
|
||||
public void Read_ShouldDeserializeNonTokenizedPaymentMethods(string typeString, NonTokenizablePaymentMethodType expectedType)
|
||||
{
|
||||
// Arrange
|
||||
var json = $"{{\"type\":\"{typeString}\"}}";
|
||||
var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };
|
||||
|
||||
// Act
|
||||
var result = JsonSerializer.Deserialize<PaymentMethod>(json, options);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsNonTokenized);
|
||||
Assert.Equal(expectedType, result.AsT1.Type);
|
||||
}
|
||||
|
||||
// Tokenized payment method serialization
|
||||
[Theory]
|
||||
[InlineData(TokenizablePaymentMethodType.BankAccount, "bankaccount")]
|
||||
[InlineData(TokenizablePaymentMethodType.Card, "card")]
|
||||
[InlineData(TokenizablePaymentMethodType.PayPal, "paypal")]
|
||||
public void Write_ShouldSerializeTokenizedPaymentMethods(TokenizablePaymentMethodType type, string expectedTypeString)
|
||||
{
|
||||
// Arrange
|
||||
var paymentMethod = new PaymentMethod(new TokenizedPaymentMethod
|
||||
{
|
||||
Type = type,
|
||||
Token = "test-token"
|
||||
});
|
||||
var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(paymentMethod, options);
|
||||
|
||||
// Assert
|
||||
Assert.Contains($"\"type\":\"{expectedTypeString}\"", json);
|
||||
Assert.Contains("\"token\":\"test-token\"", json);
|
||||
}
|
||||
|
||||
// Non-tokenized payment method serialization
|
||||
[Theory]
|
||||
[InlineData(NonTokenizablePaymentMethodType.AccountCredit, "accountcredit")]
|
||||
public void Write_ShouldSerializeNonTokenizedPaymentMethods(NonTokenizablePaymentMethodType type, string expectedTypeString)
|
||||
{
|
||||
// Arrange
|
||||
var paymentMethod = new PaymentMethod(new NonTokenizedPaymentMethod { Type = type });
|
||||
var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(paymentMethod, options);
|
||||
|
||||
// Assert
|
||||
Assert.Contains($"\"type\":\"{expectedTypeString}\"", json);
|
||||
Assert.DoesNotContain("token", json);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
@@ -567,4 +568,79 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
var unhandled = result.AsT3;
|
||||
Assert.Equal("Something went wrong with your request. Please contact support for assistance.", unhandled.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_AccountCredit_WithExistingCustomer_Success(
|
||||
User user,
|
||||
NonTokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = "existing_customer_123";
|
||||
paymentMethod.Type = NonTokenizablePaymentMethodType.AccountCredit;
|
||||
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>();
|
||||
|
||||
_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);
|
||||
await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());
|
||||
await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
|
||||
Assert.True(user.Premium);
|
||||
Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NonTokenizedPaymentWithoutExistingCustomer_ThrowsBillingException(
|
||||
User user,
|
||||
NonTokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
// No existing gateway customer ID
|
||||
user.GatewayCustomerId = null;
|
||||
paymentMethod.Type = NonTokenizablePaymentMethodType.AccountCredit;
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
//Assert
|
||||
Assert.True(result.IsT3); // Assuming T3 is the Unhandled result
|
||||
Assert.IsType<BillingException>(result.AsT3.Exception);
|
||||
// Verify no customer was created or subscription attempted
|
||||
await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
|
||||
await _stripeAdapter.DidNotReceive().SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
<None Remove="Utilities\data\embeddedResource.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Email templates uses .hbs extension, they must be included for emails to work -->
|
||||
<EmbeddedResource Include="**\*.hbs" />
|
||||
|
||||
<EmbeddedResource Include="Utilities\data\embeddedResource.txt" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
20
test/Core.Test/Platform/Mailer/HandlebarMailRendererTests.cs
Normal file
20
test/Core.Test/Platform/Mailer/HandlebarMailRendererTests.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Bit.Core.Platform.Mailer;
|
||||
using Bit.Core.Test.Platform.Mailer.TestMail;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Mailer;
|
||||
|
||||
public class HandlebarMailRendererTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RenderAsync_ReturnsExpectedHtmlAndTxt()
|
||||
{
|
||||
var renderer = new HandlebarMailRenderer();
|
||||
var view = new TestMailView { Name = "John Smith" };
|
||||
|
||||
var (html, txt) = await renderer.RenderAsync(view);
|
||||
|
||||
Assert.Equal("Hello <b>John Smith</b>", html.Trim());
|
||||
Assert.Equal("Hello John Smith", txt.Trim());
|
||||
}
|
||||
}
|
||||
37
test/Core.Test/Platform/Mailer/MailerTest.cs
Normal file
37
test/Core.Test/Platform/Mailer/MailerTest.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Platform.Mailer;
|
||||
using Bit.Core.Services;
|
||||
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 mail = new TestMail.TestMail()
|
||||
{
|
||||
ToEmails = ["test@bw.com"],
|
||||
View = new TestMailView() { Name = "John Smith" }
|
||||
};
|
||||
|
||||
MailMessage? sentMessage = null;
|
||||
await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message =>
|
||||
sentMessage = message
|
||||
));
|
||||
|
||||
await mailer.SendEmail(mail);
|
||||
|
||||
Assert.NotNull(sentMessage);
|
||||
Assert.Contains("test@bw.com", sentMessage.ToEmails);
|
||||
Assert.Equal("Test Email", sentMessage.Subject);
|
||||
Assert.Equivalent("Hello John Smith", sentMessage.TextContent.Trim());
|
||||
Assert.Equivalent("Hello <b>John Smith</b>", sentMessage.HtmlContent.Trim());
|
||||
}
|
||||
}
|
||||
13
test/Core.Test/Platform/Mailer/TestMail/TestMailView.cs
Normal file
13
test/Core.Test/Platform/Mailer/TestMail/TestMailView.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Bit.Core.Platform.Mailer;
|
||||
|
||||
namespace Bit.Core.Test.Platform.Mailer.TestMail;
|
||||
|
||||
public class TestMailView : BaseMailView
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
}
|
||||
|
||||
public class TestMail : BaseMail<TestMailView>
|
||||
{
|
||||
public override string Subject { get; } = "Test Email";
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Hello <b>{{ Name }}</b>
|
||||
@@ -0,0 +1 @@
|
||||
Hello {{ Name }}
|
||||
Reference in New Issue
Block a user