Skip to content

Commit 0dd6fd6

Browse files
test(billing): add premium checkout session tests
1 parent 2bbe6fc commit 0dd6fd6

2 files changed

Lines changed: 283 additions & 0 deletions

File tree

test/Api.Test/Billing/Controllers/VNext/AccountBillingVNextControllerTests.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
using Bit.Api.Billing.Controllers.VNext;
2+
using Bit.Api.Billing.Models.Requests.Premium;
23
using Bit.Api.Billing.Models.Requests.Storage;
4+
using Bit.Api.Billing.Models.Responses;
35
using Bit.Core.Billing.Commands;
46
using Bit.Core.Billing.Licenses.Queries;
57
using Bit.Core.Billing.Models.Api.Response;
8+
using Bit.Core.Billing.Models.Api.Response.Premium;
69
using Bit.Core.Billing.Models.Business;
710
using Bit.Core.Billing.Payment.Queries;
811
using Bit.Core.Billing.Premium.Commands;
12+
using Bit.Core.Billing.Premium.Models;
913
using Bit.Core.Billing.Subscriptions.Commands;
1014
using Bit.Core.Billing.Subscriptions.Queries;
1115
using Bit.Core.Entities;
@@ -21,6 +25,7 @@ namespace Bit.Api.Test.Billing.Controllers.VNext;
2125

2226
public class AccountBillingVNextControllerTests
2327
{
28+
private readonly ICreatePremiumCheckoutSessionCommand _createPremiumCheckoutSessionCommand;
2429
private readonly IUpdatePremiumStorageCommand _updatePremiumStorageCommand;
2530
private readonly IGetUserLicenseQuery _getUserLicenseQuery;
2631
private readonly IUpgradePremiumToOrganizationCommand _upgradePremiumToOrganizationCommand;
@@ -29,13 +34,15 @@ public class AccountBillingVNextControllerTests
2934

3035
public AccountBillingVNextControllerTests()
3136
{
37+
_createPremiumCheckoutSessionCommand = Substitute.For<ICreatePremiumCheckoutSessionCommand>();
3238
_updatePremiumStorageCommand = Substitute.For<IUpdatePremiumStorageCommand>();
3339
_getUserLicenseQuery = Substitute.For<IGetUserLicenseQuery>();
3440
_upgradePremiumToOrganizationCommand = Substitute.For<IUpgradePremiumToOrganizationCommand>();
3541
_getApplicableDiscountsQuery = Substitute.For<IGetApplicableDiscountsQuery>();
3642

3743
_sut = new AccountBillingVNextController(
3844
Substitute.For<Core.Billing.Payment.Commands.ICreateBitPayInvoiceForCreditCommand>(),
45+
_createPremiumCheckoutSessionCommand,
3946
Substitute.For<Core.Billing.Premium.Commands.ICreatePremiumCloudHostedSubscriptionCommand>(),
4047
Substitute.For<IGetBitwardenSubscriptionQuery>(),
4148
Substitute.For<Core.Billing.Payment.Queries.IGetCreditQuery>(),
@@ -257,6 +264,82 @@ public async Task UpdateStorageAsync_NullPaymentSecret_Success(User user)
257264
await _updatePremiumStorageCommand.Received(1).Run(user, 5);
258265
}
259266

267+
[Theory, BitAutoData]
268+
public async Task CreatePremiumCheckoutSessionAsync_MissingAppVersionHeader_ReturnsBadRequest(
269+
User user,
270+
CreatePremiumCheckoutSessionRequest request)
271+
{
272+
// Act
273+
var result = await _sut.CreatePremiumCheckoutSessionAsync(user, request, null);
274+
275+
// Assert
276+
Assert.IsType<BadRequest<Core.Models.Api.ErrorResponseModel>>(result);
277+
await _createPremiumCheckoutSessionCommand.DidNotReceive().Run(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>());
278+
}
279+
280+
[Theory, BitAutoData]
281+
public async Task CreatePremiumCheckoutSessionAsync_ReturnsOk(
282+
User user,
283+
CreatePremiumCheckoutSessionRequest request)
284+
{
285+
// Arrange
286+
var appVersion = "2024.1.0";
287+
var response = new PremiumCheckoutSessionResponseModel("https://checkout.stripe.com/c/pay/cs_123");
288+
289+
_createPremiumCheckoutSessionCommand
290+
.Run(user, appVersion, request.Platform)
291+
.Returns(new BillingCommandResult<PremiumCheckoutSessionResponseModel>(response));
292+
293+
// Act
294+
var result = await _sut.CreatePremiumCheckoutSessionAsync(user, request, appVersion);
295+
296+
// Assert
297+
var okResult = Assert.IsType<Ok<PremiumCheckoutSessionResponseModel>>(result);
298+
Assert.Equal(response.CheckoutSessionUrl, okResult.Value!.CheckoutSessionUrl);
299+
await _createPremiumCheckoutSessionCommand.Received(1).Run(user, appVersion, request.Platform);
300+
}
301+
302+
[Theory, BitAutoData]
303+
public async Task CreatePremiumCheckoutSessionAsync_UserIsPremium_ReturnsBadRequest(
304+
User user,
305+
CreatePremiumCheckoutSessionRequest request)
306+
{
307+
// Arrange
308+
var appVersion = "2024.1.0";
309+
var errorMessage = "User is already a premium user.";
310+
311+
_createPremiumCheckoutSessionCommand
312+
.Run(user, appVersion, request.Platform)
313+
.Returns(new BadRequest(errorMessage));
314+
315+
// Act
316+
var result = await _sut.CreatePremiumCheckoutSessionAsync(user, request, appVersion);
317+
318+
// Assert
319+
Assert.IsType<BadRequest<Core.Models.Api.ErrorResponseModel>>(result);
320+
await _createPremiumCheckoutSessionCommand.Received(1).Run(user, appVersion, request.Platform);
321+
}
322+
323+
[Theory, BitAutoData]
324+
public async Task CreatePremiumCheckoutSessionAsync_ReturnsServerError(
325+
User user,
326+
CreatePremiumCheckoutSessionRequest request)
327+
{
328+
// Arrange
329+
var appVersion = "2024.1.0";
330+
331+
_createPremiumCheckoutSessionCommand
332+
.Run(user, appVersion, request.Platform)
333+
.Returns(new Unhandled());
334+
335+
// Act
336+
var result = await _sut.CreatePremiumCheckoutSessionAsync(user, request, appVersion);
337+
338+
// Assert
339+
Assert.IsType<JsonHttpResult<Core.Models.Api.ErrorResponseModel>>(result);
340+
await _createPremiumCheckoutSessionCommand.Received(1).Run(user, appVersion, request.Platform);
341+
}
342+
260343
[Theory, BitAutoData]
261344
public async Task GetApplicableDiscountsAsync_NoEligibleDiscounts_ReturnsOkWithEmptyArray(User user)
262345
{
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
using Bit.Core.Billing;
2+
using Bit.Core.Billing.Constants;
3+
using Bit.Core.Billing.Premium.Commands;
4+
using Bit.Core.Billing.Pricing;
5+
using Bit.Core.Billing.Services;
6+
using Bit.Core.Entities;
7+
using Bit.Core.Exceptions;
8+
using Bit.Core.Services;
9+
using Bit.Core.Settings;
10+
using Bit.Test.Common.AutoFixture.Attributes;
11+
using Microsoft.Extensions.Logging;
12+
using NSubstitute;
13+
using NSubstitute.ExceptionExtensions;
14+
using Stripe;
15+
using Stripe.Checkout;
16+
using Xunit;
17+
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
18+
using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;
19+
20+
21+
namespace Bit.Core.Test.Billing.Premium.Commands;
22+
23+
public class CreatePremiumCheckoutSessionCommandTests
24+
{
25+
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
26+
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
27+
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
28+
private readonly IUserService _userService= Substitute.For<IUserService>();
29+
private readonly IGlobalSettings _globalSettings = Substitute.For<IGlobalSettings>();
30+
private readonly ILogger<CreatePremiumCheckoutSessionCommand> _logger = Substitute.For<ILogger<CreatePremiumCheckoutSessionCommand>>();
31+
private readonly ICreatePremiumCheckoutSessionCommand _command;
32+
33+
private const string _successUrl = "success/url";
34+
private const string _cancelUrl = "cancel/url";
35+
36+
public CreatePremiumCheckoutSessionCommandTests()
37+
{
38+
var stripeSettings = new GlobalSettings.StripeSettings
39+
{
40+
PremiumCheckoutSuccessUrl = _successUrl,
41+
PremiumCheckoutCancelUrl = _cancelUrl
42+
};
43+
_globalSettings.Stripe.Returns(stripeSettings);
44+
45+
var premiumPlan = new PremiumPlan
46+
{
47+
Name = "Premium",
48+
Available = true,
49+
LegacyYear = null,
50+
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
51+
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal }
52+
};
53+
54+
_pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
55+
56+
_command = new CreatePremiumCheckoutSessionCommand(
57+
_stripeAdapter,
58+
_pricingClient,
59+
_subscriberService,
60+
_userService,
61+
_globalSettings,
62+
_logger);
63+
}
64+
65+
[Theory]
66+
[BitAutoData]
67+
public async Task Run_UserNotPremium_UserDoesNotHaveExistingStripeCustomer_ReturnsCheckoutSessionUrl(User user)
68+
{
69+
// Arrange
70+
user.Premium = false;
71+
user.Name = "Test User";
72+
user.Email = "test@example.com";
73+
user.GatewayCustomerId = null;
74+
const string appVersion = "1.0.0";
75+
const string platform = "iOS";
76+
77+
const string checkoutSessionUrl = "https://checkout.stripe.com/session/123";
78+
_stripeAdapter.CreateCheckoutSessionAsync(Arg.Any<SessionCreateOptions>()).Returns(new Session { Url = checkoutSessionUrl });
79+
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(new Customer { Id = "cus_123" });
80+
81+
// Act
82+
var result = await _command.Run(user, appVersion, platform);
83+
84+
// Assert
85+
Assert.True(result.Success);
86+
Assert.Equal(checkoutSessionUrl, result.AsT0.CheckoutSessionUrl);
87+
await _stripeAdapter.Received(1).CreateCheckoutSessionAsync(Arg.Is<SessionCreateOptions>(options =>
88+
options.Customer == user.GatewayCustomerId
89+
&& options.Mode == StripeConstants.CheckoutSession.Modes.Subscription
90+
&& options.LineItems[0].Price == StripeConstants.Prices.PremiumAnnually
91+
&& options.LineItems[0].Quantity == 1
92+
&& options.AutomaticTax.Enabled == true
93+
&& options.SuccessUrl == _successUrl
94+
&& options.CancelUrl == _cancelUrl
95+
&& options.SubscriptionData.Metadata[StripeConstants.MetadataKeys.UserId] == user.Id.ToString()
96+
&& options.SubscriptionData.Metadata[StripeConstants.MetadataKeys.OriginatingAppVersion] == appVersion
97+
&& options.SubscriptionData.Metadata[StripeConstants.MetadataKeys.OriginatingPlatform] == platform));
98+
await _stripeAdapter.Received(1).CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(options =>
99+
options.Email == user.Email
100+
&& options.Description == user.Name
101+
&& options.Metadata[StripeConstants.MetadataKeys.Region] == _globalSettings.BaseServiceUri.CloudRegion));
102+
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(savedUser =>
103+
savedUser.GatewayCustomerId == "cus_123"));
104+
}
105+
106+
[Theory]
107+
[BitAutoData]
108+
public async Task Run_UserNotPremium_UserHasExistingStripeCustomer_ReturnsCheckoutSessionUrl(User user)
109+
{
110+
// Arrange
111+
user.Premium = false;
112+
user.GatewayCustomerId = "cus_existing";
113+
const string appVersion = "2.0.0";
114+
const string platform = "Android";
115+
116+
var existingCustomer = new Customer { Id = "cus_existing" };
117+
_subscriberService.GetCustomerOrThrow(user).Returns(existingCustomer);
118+
119+
const string checkoutSessionUrl = "https://checkout.stripe.com/session/456";
120+
_stripeAdapter.CreateCheckoutSessionAsync(Arg.Any<SessionCreateOptions>()).Returns(new Session { Url = checkoutSessionUrl });
121+
122+
// Act
123+
var result = await _command.Run(user, appVersion, platform);
124+
125+
// Assert
126+
Assert.True(result.Success);
127+
Assert.Equal(checkoutSessionUrl, result.AsT0.CheckoutSessionUrl);
128+
await _stripeAdapter.DidNotReceive().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
129+
await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());
130+
await _stripeAdapter.Received(1).CreateCheckoutSessionAsync(Arg.Is<SessionCreateOptions>(options =>
131+
options.Customer == existingCustomer.Id
132+
&& options.Mode == StripeConstants.CheckoutSession.Modes.Subscription
133+
&& options.LineItems[0].Price == StripeConstants.Prices.PremiumAnnually
134+
&& options.LineItems[0].Quantity == 1
135+
&& options.AutomaticTax.Enabled == true
136+
&& options.SuccessUrl == _successUrl
137+
&& options.CancelUrl == _cancelUrl
138+
&& options.SubscriptionData.Metadata[StripeConstants.MetadataKeys.UserId] == user.Id.ToString()
139+
&& options.SubscriptionData.Metadata[StripeConstants.MetadataKeys.OriginatingAppVersion] == appVersion
140+
&& options.SubscriptionData.Metadata[StripeConstants.MetadataKeys.OriginatingPlatform] == platform));
141+
}
142+
143+
[Theory]
144+
[BitAutoData]
145+
public async Task Run_UserIsPremium_ReturnsBadRequest(User user)
146+
{
147+
// Arrange
148+
user.Premium = true;
149+
150+
// Act
151+
var result = await _command.Run(user, "1.0.0", "iOS");
152+
153+
// Assert
154+
Assert.True(result.IsT1);
155+
var badRequest = result.AsT1;
156+
Assert.Equal("User is already a premium user.", badRequest.Response);
157+
await _stripeAdapter.DidNotReceive().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
158+
await _stripeAdapter.DidNotReceive().CreateCheckoutSessionAsync(Arg.Any<SessionCreateOptions>());
159+
}
160+
161+
[Theory]
162+
[BitAutoData]
163+
public async Task Run_GetCustomerOrThrowThrows_ReturnsUnhandled(User user)
164+
{
165+
// Arrange
166+
user.Premium = false;
167+
user.GatewayCustomerId = "cus_existing";
168+
169+
_subscriberService.GetCustomerOrThrow(user).ThrowsAsync(new BillingException());
170+
171+
// Act
172+
var result = await _command.Run(user, "1.0.0", "iOS");
173+
174+
// Assert
175+
Assert.True(result.IsT3);
176+
Assert.IsType<BillingException>(result.AsT3.Exception);
177+
await _stripeAdapter.DidNotReceive().CreateCheckoutSessionAsync(Arg.Any<SessionCreateOptions>());
178+
}
179+
180+
[Theory]
181+
[BitAutoData]
182+
public async Task Run_GetAvailablePremiumPlanThrows_ReturnsUnhandled(User user)
183+
{
184+
// Arrange
185+
user.Premium = false;
186+
user.GatewayCustomerId = null;
187+
188+
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(new Customer { Id = "cus_123" });
189+
_pricingClient.GetAvailablePremiumPlan().ThrowsAsync<NotFoundException>();
190+
191+
// Act
192+
var result = await _command.Run(user, "1.0.0", "iOS");
193+
194+
// Assert
195+
Assert.True(result.IsT3); // UnhandledException
196+
Assert.IsType<NotFoundException>(result.AsT3.Exception);
197+
await _stripeAdapter.DidNotReceive().CreateCheckoutSessionAsync(Arg.Any<SessionCreateOptions>());
198+
}
199+
200+
}

0 commit comments

Comments
 (0)