using Bit.Core.Billing.Caches; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Premium.Commands; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Platform.Push; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture.Attributes; using Braintree; using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; using Xunit; using Address = Stripe.Address; using StripeCustomer = Stripe.Customer; using StripeSubscription = Stripe.Subscription; namespace Bit.Core.Test.Billing.Premium.Commands; public class CreatePremiumCloudHostedSubscriptionCommandTests { private readonly IBraintreeGateway _braintreeGateway = Substitute.For(); private readonly IGlobalSettings _globalSettings = Substitute.For(); private readonly ISetupIntentCache _setupIntentCache = Substitute.For(); private readonly IStripeAdapter _stripeAdapter = Substitute.For(); private readonly ISubscriberService _subscriberService = Substitute.For(); private readonly IUserService _userService = Substitute.For(); private readonly IPushNotificationService _pushNotificationService = Substitute.For(); private readonly CreatePremiumCloudHostedSubscriptionCommand _command; public CreatePremiumCloudHostedSubscriptionCommandTests() { var baseServiceUri = Substitute.For(); baseServiceUri.CloudRegion.Returns("US"); _globalSettings.BaseServiceUri.Returns(baseServiceUri); _command = new CreatePremiumCloudHostedSubscriptionCommand( _braintreeGateway, _globalSettings, _setupIntentCache, _stripeAdapter, _subscriberService, _userService, _pushNotificationService, Substitute.For>()); } [Theory, BitAutoData] public async Task Run_UserAlreadyPremium_ReturnsBadRequest( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = true; // Act var result = await _command.Run(user, paymentMethod, billingAddress, 0); // Assert Assert.True(result.IsT1); var badRequest = result.AsT1; Assert.Equal("Already a premium user.", badRequest.Response); } [Theory, BitAutoData] public async Task Run_NegativeStorageAmount_ReturnsBadRequest( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = false; // Act var result = await _command.Run(user, paymentMethod, billingAddress, -1); // Assert Assert.True(result.IsT1); var badRequest = result.AsT1; Assert.Equal("Additional storage must be greater than 0.", badRequest.Response); } [Theory, BitAutoData] public async Task Run_ValidPaymentMethodTypes_BankAccount_Success( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = false; user.GatewayCustomerId = null; // Ensure no existing customer ID user.Email = "test@example.com"; paymentMethod.Type = TokenizablePaymentMethodType.BankAccount; paymentMethod.Token = "bank_token_123"; billingAddress.Country = "US"; billingAddress.PostalCode = "12345"; var mockCustomer = Substitute.For(); mockCustomer.Id = "cust_123"; mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; mockCustomer.Metadata = new Dictionary(); var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; mockSubscription.Items = new StripeList { Data = [ new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } ] }; var mockInvoice = Substitute.For(); var mockSetupIntent = Substitute.For(); mockSetupIntent.Id = "seti_123"; _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); _stripeAdapter.SetupIntentList(Arg.Any()).Returns(Task.FromResult(new List { mockSetupIntent })); _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); // Act var result = await _command.Run(user, paymentMethod, billingAddress, 0); // Assert Assert.True(result.IsT0); await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any()); await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any()); await _userService.Received(1).SaveUserAsync(user); await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id); } [Theory, BitAutoData] public async Task Run_ValidPaymentMethodTypes_Card_Success( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = false; user.GatewayCustomerId = null; user.Email = "test@example.com"; paymentMethod.Type = TokenizablePaymentMethodType.Card; paymentMethod.Token = "card_token_123"; billingAddress.Country = "US"; billingAddress.PostalCode = "12345"; var mockCustomer = Substitute.For(); mockCustomer.Id = "cust_123"; mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; mockCustomer.Metadata = new Dictionary(); var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; mockSubscription.Items = new StripeList { Data = [ new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } ] }; var mockInvoice = Substitute.For(); _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); // Act var result = await _command.Run(user, paymentMethod, billingAddress, 0); // Assert Assert.True(result.IsT0); await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any()); await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any()); await _userService.Received(1).SaveUserAsync(user); await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id); } [Theory, BitAutoData] public async Task Run_ValidPaymentMethodTypes_PayPal_Success( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = false; user.GatewayCustomerId = null; user.Email = "test@example.com"; paymentMethod.Type = TokenizablePaymentMethodType.PayPal; paymentMethod.Token = "paypal_token_123"; billingAddress.Country = "US"; billingAddress.PostalCode = "12345"; var mockCustomer = Substitute.For(); mockCustomer.Id = "cust_123"; mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; mockCustomer.Metadata = new Dictionary(); var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; var mockInvoice = Substitute.For(); _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); _subscriberService.CreateBraintreeCustomer(Arg.Any(), Arg.Any()).Returns("bt_customer_123"); // Act var result = await _command.Run(user, paymentMethod, billingAddress, 0); // Assert Assert.True(result.IsT0); await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any()); await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any()); await _subscriberService.Received(1).CreateBraintreeCustomer(user, paymentMethod.Token); await _userService.Received(1).SaveUserAsync(user); await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id); } [Theory, BitAutoData] public async Task Run_ValidRequestWithAdditionalStorage_Success( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = false; user.GatewayCustomerId = null; user.Email = "test@example.com"; paymentMethod.Type = TokenizablePaymentMethodType.Card; paymentMethod.Token = "card_token_123"; billingAddress.Country = "US"; billingAddress.PostalCode = "12345"; const short additionalStorage = 2; var mockCustomer = Substitute.For(); mockCustomer.Id = "cust_123"; mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; mockCustomer.Metadata = new Dictionary(); var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; mockSubscription.Items = new StripeList { Data = [ new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } ] }; var mockInvoice = Substitute.For(); _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); // Act var result = await _command.Run(user, paymentMethod, billingAddress, additionalStorage); // Assert Assert.True(result.IsT0); Assert.True(user.Premium); Assert.Equal((short)(1 + additionalStorage), user.MaxStorageGb); Assert.NotNull(user.LicenseKey); Assert.Equal(20, user.LicenseKey.Length); Assert.NotEqual(default, user.RevisionDate); await _userService.Received(1).SaveUserAsync(user); await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id); } [Theory, BitAutoData] public async Task Run_UserHasExistingGatewayCustomerId_UsesExistingCustomer( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = false; user.GatewayCustomerId = "existing_customer_123"; paymentMethod.Type = TokenizablePaymentMethodType.Card; billingAddress.Country = "US"; billingAddress.PostalCode = "12345"; var mockCustomer = Substitute.For(); mockCustomer.Id = "existing_customer_123"; mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; mockCustomer.Metadata = new Dictionary(); var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; mockSubscription.Items = new StripeList { Data = [ new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } ] }; var mockInvoice = Substitute.For(); _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); // Act var result = await _command.Run(user, paymentMethod, billingAddress, 0); // Assert Assert.True(result.IsT0); await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any(), Arg.Any()); await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any()); } [Theory, BitAutoData] public async Task Run_PayPalWithIncompleteSubscription_SetsPremiumTrue( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = false; user.GatewayCustomerId = null; user.Email = "test@example.com"; user.PremiumExpirationDate = null; paymentMethod.Type = TokenizablePaymentMethodType.PayPal; paymentMethod.Token = "paypal_token_123"; billingAddress.Country = "US"; billingAddress.PostalCode = "12345"; var mockCustomer = Substitute.For(); mockCustomer.Id = "cust_123"; mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; mockCustomer.Metadata = new Dictionary(); var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "incomplete"; mockSubscription.Items = new StripeList { Data = [ new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } ] }; var mockInvoice = Substitute.For(); _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); _subscriberService.CreateBraintreeCustomer(Arg.Any(), Arg.Any()).Returns("bt_customer_123"); // Act var result = await _command.Run(user, paymentMethod, billingAddress, 0); // Assert Assert.True(result.IsT0); Assert.True(user.Premium); Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate); } [Theory, BitAutoData] public async Task Run_NonPayPalWithActiveSubscription_SetsPremiumTrue( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = false; user.GatewayCustomerId = null; user.Email = "test@example.com"; paymentMethod.Type = TokenizablePaymentMethodType.Card; paymentMethod.Token = "card_token_123"; billingAddress.Country = "US"; billingAddress.PostalCode = "12345"; var mockCustomer = Substitute.For(); mockCustomer.Id = "cust_123"; mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; mockCustomer.Metadata = new Dictionary(); var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; mockSubscription.Items = new StripeList { Data = [ new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } ] }; var mockInvoice = Substitute.For(); _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); // Act var result = await _command.Run(user, paymentMethod, billingAddress, 0); // Assert Assert.True(result.IsT0); Assert.True(user.Premium); Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate); } [Theory, BitAutoData] public async Task Run_SubscriptionStatusDoesNotMatchPatterns_DoesNotSetPremium( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = false; user.GatewayCustomerId = null; user.Email = "test@example.com"; user.PremiumExpirationDate = null; paymentMethod.Type = TokenizablePaymentMethodType.PayPal; paymentMethod.Token = "paypal_token_123"; billingAddress.Country = "US"; billingAddress.PostalCode = "12345"; var mockCustomer = Substitute.For(); mockCustomer.Id = "cust_123"; mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; mockCustomer.Metadata = new Dictionary(); var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; // PayPal + active doesn't match pattern mockSubscription.Items = new StripeList { Data = [ new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } ] }; var mockInvoice = Substitute.For(); _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); _subscriberService.CreateBraintreeCustomer(Arg.Any(), Arg.Any()).Returns("bt_customer_123"); // Act var result = await _command.Run(user, paymentMethod, billingAddress, 0); // Assert Assert.True(result.IsT0); Assert.False(user.Premium); Assert.Null(user.PremiumExpirationDate); } [Theory, BitAutoData] public async Task Run_BankAccountWithNoSetupIntentFound_ReturnsUnhandled( User user, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { // Arrange user.Premium = false; user.GatewayCustomerId = null; user.Email = "test@example.com"; paymentMethod.Type = TokenizablePaymentMethodType.BankAccount; paymentMethod.Token = "bank_token_123"; billingAddress.Country = "US"; billingAddress.PostalCode = "12345"; var mockCustomer = Substitute.For(); mockCustomer.Id = "cust_123"; mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; mockCustomer.Metadata = new Dictionary(); var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "incomplete"; mockSubscription.Items = new StripeList { Data = [ new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } ] }; var mockInvoice = Substitute.For(); _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); _stripeAdapter.SetupIntentList(Arg.Any()) .Returns(Task.FromResult(new List())); // Empty list - no setup intent found // Act var result = await _command.Run(user, paymentMethod, billingAddress, 0); // Assert Assert.True(result.IsT3); var unhandled = result.AsT3; Assert.Equal("Something went wrong with your request. Please contact support for assistance.", unhandled.Response); } }