Browse Source
* The discount badge implementation * Address the claude pr comments * Add more unit testing * Add more test * used existing flag * Add the coupon Ids * Add more code documentation * Add some recommendation from claude * Fix addition comments and prs * Add more integration test * Fix some comment and add more test * rename the test methods * Add more unit test and comments * Resolve the null issues * Add more test * reword the comments * Rename Variable * Some code refactoring * Change the coupon ID to milestone-2c * Fix the failing Testpull/6576/head
10 changed files with 2460 additions and 59 deletions
@ -0,0 +1,800 @@
@@ -0,0 +1,800 @@
|
||||
using System.Security.Claims; |
||||
using Bit.Api.Billing.Controllers; |
||||
using Bit.Core; |
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; |
||||
using Bit.Core.Billing.Constants; |
||||
using Bit.Core.Billing.Models.Business; |
||||
using Bit.Core.Entities; |
||||
using Bit.Core.Enums; |
||||
using Bit.Core.KeyManagement.Queries.Interfaces; |
||||
using Bit.Core.Models.Business; |
||||
using Bit.Core.Services; |
||||
using Bit.Core.Settings; |
||||
using Bit.Core.Test.AutoFixture; |
||||
using Bit.Test.Common.AutoFixture.Attributes; |
||||
using Microsoft.AspNetCore.Http; |
||||
using Microsoft.AspNetCore.Mvc; |
||||
using NSubstitute; |
||||
using Stripe; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Api.Test.Billing.Controllers; |
||||
|
||||
[SubscriptionInfoCustomize] |
||||
public class AccountsControllerTests : IDisposable |
||||
{ |
||||
private const string TestMilestone2CouponId = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount; |
||||
|
||||
private readonly IUserService _userService; |
||||
private readonly IFeatureService _featureService; |
||||
private readonly IPaymentService _paymentService; |
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; |
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery; |
||||
private readonly GlobalSettings _globalSettings; |
||||
private readonly AccountsController _sut; |
||||
|
||||
public AccountsControllerTests() |
||||
{ |
||||
_userService = Substitute.For<IUserService>(); |
||||
_featureService = Substitute.For<IFeatureService>(); |
||||
_paymentService = Substitute.For<IPaymentService>(); |
||||
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>(); |
||||
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>(); |
||||
_globalSettings = new GlobalSettings { SelfHosted = false }; |
||||
|
||||
_sut = new AccountsController( |
||||
_userService, |
||||
_twoFactorIsEnabledQuery, |
||||
_userAccountKeysQuery, |
||||
_featureService |
||||
); |
||||
} |
||||
|
||||
public void Dispose() |
||||
{ |
||||
_sut?.Dispose(); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_WhenFeatureFlagEnabled_IncludesDiscount( |
||||
User user, |
||||
SubscriptionInfo subscriptionInfo, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
Active = true, |
||||
PercentOff = 20m, |
||||
AmountOff = null, |
||||
AppliesTo = new List<string> { "product1" } |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result); |
||||
Assert.NotNull(result.CustomerDiscount); |
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); |
||||
Assert.Equal(20m, result.CustomerDiscount.PercentOff); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_WhenFeatureFlagDisabled_ExcludesDiscount( |
||||
User user, |
||||
SubscriptionInfo subscriptionInfo, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
Active = true, |
||||
PercentOff = 20m, |
||||
AmountOff = null, |
||||
AppliesTo = new List<string> { "product1" } |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(false); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result); |
||||
Assert.Null(result.CustomerDiscount); // Should be null when feature flag is disabled |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_WithNonMatchingCouponId_ExcludesDiscount( |
||||
User user, |
||||
SubscriptionInfo subscriptionInfo, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = "different-coupon-id", // Non-matching coupon ID |
||||
Active = true, |
||||
PercentOff = 20m, |
||||
AmountOff = null, |
||||
AppliesTo = new List<string> { "product1" } |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result); |
||||
Assert.Null(result.CustomerDiscount); // Should be null when coupon ID doesn't match |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_WhenSelfHosted_ReturnsBasicResponse(User user) |
||||
{ |
||||
// Arrange |
||||
var selfHostedSettings = new GlobalSettings { SelfHosted = true }; |
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(selfHostedSettings, _paymentService); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result); |
||||
Assert.Null(result.CustomerDiscount); |
||||
await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>()); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_WhenNoGateway_ExcludesDiscount(User user, UserLicense license) |
||||
{ |
||||
// Arrange |
||||
user.Gateway = null; // No gateway configured |
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_userService.GenerateLicenseAsync(user).Returns(license); |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result); |
||||
Assert.Null(result.CustomerDiscount); // Should be null when no gateway |
||||
await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>()); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_WithInactiveDiscount_ExcludesDiscount( |
||||
User user, |
||||
SubscriptionInfo subscriptionInfo, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
Active = false, // Inactive discount |
||||
PercentOff = 20m, |
||||
AmountOff = null, |
||||
AppliesTo = new List<string> { "product1" } |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result); |
||||
Assert.Null(result.CustomerDiscount); // Should be null when discount is inactive |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_FullPipeline_ConvertsStripeDiscountToApiResponse( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Create a Stripe Discount object with real structure |
||||
var stripeDiscount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
PercentOff = 25m, |
||||
AmountOff = 1400, // 1400 cents = $14.00 |
||||
AppliesTo = new CouponAppliesTo |
||||
{ |
||||
Products = new List<string> { "prod_premium", "prod_families" } |
||||
} |
||||
}, |
||||
End = null // Active discount |
||||
}; |
||||
|
||||
// Convert Stripe Discount to BillingCustomerDiscount (simulating what StripePaymentService does) |
||||
var billingDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount); |
||||
|
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = billingDiscount |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
|
||||
user.Gateway = GatewayType.Stripe; |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert - Verify full pipeline conversion |
||||
Assert.NotNull(result); |
||||
Assert.NotNull(result.CustomerDiscount); |
||||
|
||||
// Verify Stripe data correctly converted to API response |
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); |
||||
Assert.True(result.CustomerDiscount.Active); |
||||
Assert.Equal(25m, result.CustomerDiscount.PercentOff); |
||||
|
||||
// Verify cents-to-dollars conversion (1400 cents -> $14.00) |
||||
Assert.Equal(14.00m, result.CustomerDiscount.AmountOff); |
||||
|
||||
// Verify AppliesTo products are preserved |
||||
Assert.NotNull(result.CustomerDiscount.AppliesTo); |
||||
Assert.Equal(2, result.CustomerDiscount.AppliesTo.Count()); |
||||
Assert.Contains("prod_premium", result.CustomerDiscount.AppliesTo); |
||||
Assert.Contains("prod_families", result.CustomerDiscount.AppliesTo); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_FullPipeline_WithFeatureFlagToggle_ControlsVisibility( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Create Stripe Discount |
||||
var stripeDiscount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
PercentOff = 20m |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
var billingDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount); |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = billingDiscount |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
user.Gateway = GatewayType.Stripe; |
||||
|
||||
// Act & Assert - Feature flag ENABLED |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
var resultWithFlag = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
Assert.NotNull(resultWithFlag.CustomerDiscount); |
||||
|
||||
// Act & Assert - Feature flag DISABLED |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(false); |
||||
var resultWithoutFlag = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
Assert.Null(resultWithoutFlag.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_IntegrationTest_CompletePipelineFromStripeToApiResponse( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Create a real Stripe Discount object as it would come from Stripe API |
||||
var stripeDiscount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
PercentOff = 30m, |
||||
AmountOff = 2000, // 2000 cents = $20.00 |
||||
AppliesTo = new CouponAppliesTo |
||||
{ |
||||
Products = new List<string> { "prod_premium", "prod_families", "prod_teams" } |
||||
} |
||||
}, |
||||
End = null // Active discount (no end date) |
||||
}; |
||||
|
||||
// Step 1: Map Stripe Discount through SubscriptionInfo.BillingCustomerDiscount |
||||
// This simulates what StripePaymentService.GetSubscriptionAsync does |
||||
var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount); |
||||
|
||||
// Verify the mapping worked correctly |
||||
Assert.Equal(TestMilestone2CouponId, billingCustomerDiscount.Id); |
||||
Assert.True(billingCustomerDiscount.Active); |
||||
Assert.Equal(30m, billingCustomerDiscount.PercentOff); |
||||
Assert.Equal(20.00m, billingCustomerDiscount.AmountOff); // Converted from cents |
||||
Assert.NotNull(billingCustomerDiscount.AppliesTo); |
||||
Assert.Equal(3, billingCustomerDiscount.AppliesTo.Count); |
||||
|
||||
// Step 2: Create SubscriptionInfo with the mapped discount |
||||
// This simulates what StripePaymentService returns |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = billingCustomerDiscount |
||||
}; |
||||
|
||||
// Step 3: Set up controller dependencies |
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
user.Gateway = GatewayType.Stripe; |
||||
|
||||
// Act - Step 4: Call AccountsController.GetSubscriptionAsync |
||||
// This exercises the complete pipeline: |
||||
// - Retrieves subscriptionInfo from paymentService (with discount from Stripe) |
||||
// - Maps through SubscriptionInfo.BillingCustomerDiscount (already done above) |
||||
// - Filters in SubscriptionResponseModel constructor (based on feature flag, coupon ID, active status) |
||||
// - Returns via AccountsController |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert - Verify the complete pipeline worked end-to-end |
||||
Assert.NotNull(result); |
||||
Assert.NotNull(result.CustomerDiscount); |
||||
|
||||
// Verify Stripe Discount → SubscriptionInfo.BillingCustomerDiscount mapping |
||||
// (verified above, but confirming it made it through) |
||||
|
||||
// Verify SubscriptionInfo.BillingCustomerDiscount → SubscriptionResponseModel.BillingCustomerDiscount filtering |
||||
// The filter should pass because: |
||||
// - includeMilestone2Discount = true (feature flag enabled) |
||||
// - subscription.CustomerDiscount != null |
||||
// - subscription.CustomerDiscount.Id == Milestone2SubscriptionDiscount |
||||
// - subscription.CustomerDiscount.Active = true |
||||
Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id); |
||||
Assert.True(result.CustomerDiscount.Active); |
||||
Assert.Equal(30m, result.CustomerDiscount.PercentOff); |
||||
Assert.Equal(20.00m, result.CustomerDiscount.AmountOff); // Verify cents-to-dollars conversion |
||||
|
||||
// Verify AppliesTo products are preserved through the entire pipeline |
||||
Assert.NotNull(result.CustomerDiscount.AppliesTo); |
||||
Assert.Equal(3, result.CustomerDiscount.AppliesTo.Count()); |
||||
Assert.Contains("prod_premium", result.CustomerDiscount.AppliesTo); |
||||
Assert.Contains("prod_families", result.CustomerDiscount.AppliesTo); |
||||
Assert.Contains("prod_teams", result.CustomerDiscount.AppliesTo); |
||||
|
||||
// Verify the payment service was called correctly |
||||
await _paymentService.Received(1).GetSubscriptionAsync(user); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_IntegrationTest_MultipleDiscountsInSubscription_PrefersCustomerDiscount( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Create Stripe subscription with multiple discounts |
||||
// Customer discount should be preferred over subscription discounts |
||||
var customerDiscount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
PercentOff = 30m, |
||||
AmountOff = null |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
var subscriptionDiscount1 = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = "other-coupon-1", |
||||
PercentOff = 10m |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
var subscriptionDiscount2 = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = "other-coupon-2", |
||||
PercentOff = 15m |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Map through SubscriptionInfo.BillingCustomerDiscount |
||||
var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(customerDiscount); |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = billingCustomerDiscount |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
user.Gateway = GatewayType.Stripe; |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert - Should use customer discount, not subscription discounts |
||||
Assert.NotNull(result); |
||||
Assert.NotNull(result.CustomerDiscount); |
||||
Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id); |
||||
Assert.Equal(30m, result.CustomerDiscount.PercentOff); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_IntegrationTest_BothPercentOffAndAmountOffPresent_HandlesEdgeCase( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Edge case: Stripe coupon with both PercentOff and AmountOff |
||||
// This tests the scenario mentioned in BillingCustomerDiscountTests.cs line 212-232 |
||||
var stripeDiscount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
PercentOff = 25m, |
||||
AmountOff = 2000, // 2000 cents = $20.00 |
||||
AppliesTo = new CouponAppliesTo |
||||
{ |
||||
Products = new List<string> { "prod_premium" } |
||||
} |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Map through SubscriptionInfo.BillingCustomerDiscount |
||||
var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount); |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = billingCustomerDiscount |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
user.Gateway = GatewayType.Stripe; |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert - Both values should be preserved through the pipeline |
||||
Assert.NotNull(result); |
||||
Assert.NotNull(result.CustomerDiscount); |
||||
Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id); |
||||
Assert.Equal(25m, result.CustomerDiscount.PercentOff); |
||||
Assert.Equal(20.00m, result.CustomerDiscount.AmountOff); // Converted from cents |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_IntegrationTest_BillingSubscriptionMapsThroughPipeline( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Create Stripe subscription with subscription details |
||||
var stripeSubscription = new Subscription |
||||
{ |
||||
Id = "sub_test123", |
||||
Status = "active", |
||||
TrialStart = DateTime.UtcNow.AddDays(-30), |
||||
TrialEnd = DateTime.UtcNow.AddDays(-20), |
||||
CanceledAt = null, |
||||
CancelAtPeriodEnd = false, |
||||
CollectionMethod = "charge_automatically" |
||||
}; |
||||
|
||||
// Map through SubscriptionInfo.BillingSubscription |
||||
var billingSubscription = new SubscriptionInfo.BillingSubscription(stripeSubscription); |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
Subscription = billingSubscription, |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
Active = true, |
||||
PercentOff = 20m |
||||
} |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
user.Gateway = GatewayType.Stripe; |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert - Verify BillingSubscription mapped through pipeline |
||||
Assert.NotNull(result); |
||||
Assert.NotNull(result.Subscription); |
||||
Assert.Equal("active", result.Subscription.Status); |
||||
Assert.Equal(14, result.Subscription.GracePeriod); // charge_automatically = 14 days |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_IntegrationTest_BillingUpcomingInvoiceMapsThroughPipeline( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Create Stripe invoice for upcoming invoice |
||||
var stripeInvoice = new Invoice |
||||
{ |
||||
AmountDue = 2000, // 2000 cents = $20.00 |
||||
Created = DateTime.UtcNow.AddDays(1) |
||||
}; |
||||
|
||||
// Map through SubscriptionInfo.BillingUpcomingInvoice |
||||
var billingUpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(stripeInvoice); |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
UpcomingInvoice = billingUpcomingInvoice, |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
Active = true, |
||||
PercentOff = 20m |
||||
} |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
user.Gateway = GatewayType.Stripe; |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert - Verify BillingUpcomingInvoice mapped through pipeline |
||||
Assert.NotNull(result); |
||||
Assert.NotNull(result.UpcomingInvoice); |
||||
Assert.Equal(20.00m, result.UpcomingInvoice.Amount); // Converted from cents |
||||
Assert.NotNull(result.UpcomingInvoice.Date); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_IntegrationTest_CompletePipelineWithAllComponents( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Complete Stripe objects for full pipeline test |
||||
var stripeDiscount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = TestMilestone2CouponId, |
||||
PercentOff = 20m, |
||||
AmountOff = 1000, // $10.00 |
||||
AppliesTo = new CouponAppliesTo |
||||
{ |
||||
Products = new List<string> { "prod_premium", "prod_families" } |
||||
} |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
var stripeSubscription = new Subscription |
||||
{ |
||||
Id = "sub_test123", |
||||
Status = "active", |
||||
CollectionMethod = "charge_automatically" |
||||
}; |
||||
|
||||
var stripeInvoice = new Invoice |
||||
{ |
||||
AmountDue = 1500, // $15.00 |
||||
Created = DateTime.UtcNow.AddDays(7) |
||||
}; |
||||
|
||||
// Map through SubscriptionInfo (simulating StripePaymentService) |
||||
var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount); |
||||
var billingSubscription = new SubscriptionInfo.BillingSubscription(stripeSubscription); |
||||
var billingUpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(stripeInvoice); |
||||
|
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = billingCustomerDiscount, |
||||
Subscription = billingSubscription, |
||||
UpcomingInvoice = billingUpcomingInvoice |
||||
}; |
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); |
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo); |
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license); |
||||
user.Gateway = GatewayType.Stripe; |
||||
|
||||
// Act - Full pipeline: Stripe → SubscriptionInfo → SubscriptionResponseModel → API response |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert - Verify all components mapped correctly through the pipeline |
||||
Assert.NotNull(result); |
||||
|
||||
// Verify discount |
||||
Assert.NotNull(result.CustomerDiscount); |
||||
Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id); |
||||
Assert.Equal(20m, result.CustomerDiscount.PercentOff); |
||||
Assert.Equal(10.00m, result.CustomerDiscount.AmountOff); |
||||
Assert.NotNull(result.CustomerDiscount.AppliesTo); |
||||
Assert.Equal(2, result.CustomerDiscount.AppliesTo.Count()); |
||||
|
||||
// Verify subscription |
||||
Assert.NotNull(result.Subscription); |
||||
Assert.Equal("active", result.Subscription.Status); |
||||
Assert.Equal(14, result.Subscription.GracePeriod); |
||||
|
||||
// Verify upcoming invoice |
||||
Assert.NotNull(result.UpcomingInvoice); |
||||
Assert.Equal(15.00m, result.UpcomingInvoice.Amount); |
||||
Assert.NotNull(result.UpcomingInvoice.Date); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_SelfHosted_WithDiscountFlagEnabled_NeverIncludesDiscount(User user) |
||||
{ |
||||
// Arrange - Self-hosted user with discount flag enabled (should still return null) |
||||
var selfHostedSettings = new GlobalSettings { SelfHosted = true }; |
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); // Flag enabled |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(selfHostedSettings, _paymentService); |
||||
|
||||
// Assert - Should never include discount for self-hosted, even with flag enabled |
||||
Assert.NotNull(result); |
||||
Assert.Null(result.CustomerDiscount); |
||||
await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>()); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public async Task GetSubscriptionAsync_NullGateway_WithDiscountFlagEnabled_NeverIncludesDiscount( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - User with null gateway and discount flag enabled (should still return null) |
||||
user.Gateway = null; // No gateway configured |
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); |
||||
_sut.ControllerContext = new ControllerContext |
||||
{ |
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal } |
||||
}; |
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); |
||||
_userService.GenerateLicenseAsync(user).Returns(license); |
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); // Flag enabled |
||||
|
||||
// Act |
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService); |
||||
|
||||
// Assert - Should never include discount when no gateway, even with flag enabled |
||||
Assert.NotNull(result); |
||||
Assert.Null(result.CustomerDiscount); |
||||
await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>()); |
||||
} |
||||
} |
||||
@ -0,0 +1,400 @@
@@ -0,0 +1,400 @@
|
||||
using Bit.Api.Models.Response; |
||||
using Bit.Core.Billing.Constants; |
||||
using Bit.Core.Billing.Models.Business; |
||||
using Bit.Core.Entities; |
||||
using Bit.Core.Models.Business; |
||||
using Bit.Test.Common.AutoFixture.Attributes; |
||||
using Stripe; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Api.Test.Models.Response; |
||||
|
||||
public class SubscriptionResponseModelTests |
||||
{ |
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_IncludeMilestone2DiscountTrueMatchingCouponId_ReturnsDiscount( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID |
||||
Active = true, |
||||
PercentOff = 20m, |
||||
AmountOff = null, |
||||
AppliesTo = new List<string> { "product1" } |
||||
} |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result.CustomerDiscount); |
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); |
||||
Assert.True(result.CustomerDiscount.Active); |
||||
Assert.Equal(20m, result.CustomerDiscount.PercentOff); |
||||
Assert.Null(result.CustomerDiscount.AmountOff); |
||||
Assert.NotNull(result.CustomerDiscount.AppliesTo); |
||||
Assert.Single(result.CustomerDiscount.AppliesTo); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_IncludeMilestone2DiscountTrueNonMatchingCouponId_ReturnsNull( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = "different-coupon-id", // Non-matching coupon ID |
||||
Active = true, |
||||
PercentOff = 20m, |
||||
AmountOff = null, |
||||
AppliesTo = new List<string> { "product1" } |
||||
} |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); |
||||
|
||||
// Assert |
||||
Assert.Null(result.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_IncludeMilestone2DiscountFalseMatchingCouponId_ReturnsNull( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID |
||||
Active = true, |
||||
PercentOff = 20m, |
||||
AmountOff = null, |
||||
AppliesTo = new List<string> { "product1" } |
||||
} |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: false); |
||||
|
||||
// Assert - Should be null because includeMilestone2Discount is false |
||||
Assert.Null(result.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_NullCustomerDiscount_ReturnsNull( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); |
||||
|
||||
// Assert |
||||
Assert.Null(result.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_AmountOffDiscountMatchingCouponId_ReturnsDiscount( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, |
||||
Active = true, |
||||
PercentOff = null, |
||||
AmountOff = 14.00m, // Already converted from cents in BillingCustomerDiscount |
||||
AppliesTo = new List<string>() |
||||
} |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result.CustomerDiscount); |
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); |
||||
Assert.Null(result.CustomerDiscount.PercentOff); |
||||
Assert.Equal(14.00m, result.CustomerDiscount.AmountOff); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_DefaultIncludeMilestone2DiscountParameter_ReturnsNull( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, |
||||
Active = true, |
||||
PercentOff = 20m |
||||
} |
||||
}; |
||||
|
||||
// Act - Using default parameter (includeMilestone2Discount defaults to false) |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license); |
||||
|
||||
// Assert |
||||
Assert.Null(result.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_NullDiscountIdIncludeMilestone2DiscountTrue_ReturnsNull( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = null, // Null discount ID |
||||
Active = true, |
||||
PercentOff = 20m, |
||||
AmountOff = null, |
||||
AppliesTo = new List<string> { "product1" } |
||||
} |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); |
||||
|
||||
// Assert |
||||
Assert.Null(result.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_MatchingCouponIdInactiveDiscount_ReturnsNull( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID |
||||
Active = false, // Inactive discount |
||||
PercentOff = 20m, |
||||
AmountOff = null, |
||||
AppliesTo = new List<string> { "product1" } |
||||
} |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); |
||||
|
||||
// Assert |
||||
Assert.Null(result.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_UserOnly_SetsBasicProperties(User user) |
||||
{ |
||||
// Arrange |
||||
user.Storage = 5368709120; // 5 GB in bytes |
||||
user.MaxStorageGb = (short)10; |
||||
user.PremiumExpirationDate = DateTime.UtcNow.AddMonths(12); |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result.StorageName); |
||||
Assert.Equal(5.0, result.StorageGb); |
||||
Assert.Equal((short)10, result.MaxStorageGb); |
||||
Assert.Equal(user.PremiumExpirationDate, result.Expiration); |
||||
Assert.Null(result.License); |
||||
Assert.Null(result.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_UserAndLicense_IncludesLicense(User user, UserLicense license) |
||||
{ |
||||
// Arrange |
||||
user.Storage = 1073741824; // 1 GB in bytes |
||||
user.MaxStorageGb = (short)5; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, license); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result.License); |
||||
Assert.Equal(license, result.License); |
||||
Assert.Equal(1.0, result.StorageGb); |
||||
Assert.Null(result.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_NullStorage_SetsStorageToZero(User user) |
||||
{ |
||||
// Arrange |
||||
user.Storage = null; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user); |
||||
|
||||
// Assert |
||||
Assert.Null(result.StorageName); |
||||
Assert.Equal(0, result.StorageGb); |
||||
Assert.Null(result.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_NullLicense_ExcludesLicense(User user) |
||||
{ |
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, null); |
||||
|
||||
// Assert |
||||
Assert.Null(result.License); |
||||
Assert.Null(result.CustomerDiscount); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_BothPercentOffAndAmountOffPresent_HandlesEdgeCase( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Edge case: Both PercentOff and AmountOff present |
||||
// This tests the scenario where Stripe coupon has both discount types |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, |
||||
Active = true, |
||||
PercentOff = 25m, |
||||
AmountOff = 20.00m, // Already converted from cents |
||||
AppliesTo = new List<string> { "prod_premium" } |
||||
} |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); |
||||
|
||||
// Assert - Both values should be preserved |
||||
Assert.NotNull(result.CustomerDiscount); |
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); |
||||
Assert.Equal(25m, result.CustomerDiscount.PercentOff); |
||||
Assert.Equal(20.00m, result.CustomerDiscount.AmountOff); |
||||
Assert.NotNull(result.CustomerDiscount.AppliesTo); |
||||
Assert.Single(result.CustomerDiscount.AppliesTo); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_WithSubscriptionAndInvoice_MapsAllProperties( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Test with Subscription, UpcomingInvoice, and CustomerDiscount |
||||
var stripeSubscription = new Subscription |
||||
{ |
||||
Id = "sub_test123", |
||||
Status = "active", |
||||
CollectionMethod = "charge_automatically" |
||||
}; |
||||
|
||||
var stripeInvoice = new Invoice |
||||
{ |
||||
AmountDue = 1500, // 1500 cents = $15.00 |
||||
Created = DateTime.UtcNow.AddDays(7) |
||||
}; |
||||
|
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
Subscription = new SubscriptionInfo.BillingSubscription(stripeSubscription), |
||||
UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(stripeInvoice), |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, |
||||
Active = true, |
||||
PercentOff = 20m, |
||||
AmountOff = null, |
||||
AppliesTo = new List<string> { "prod_premium" } |
||||
} |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); |
||||
|
||||
// Assert - Verify all properties are mapped correctly |
||||
Assert.NotNull(result.Subscription); |
||||
Assert.Equal("active", result.Subscription.Status); |
||||
Assert.Equal(14, result.Subscription.GracePeriod); // charge_automatically = 14 days |
||||
|
||||
Assert.NotNull(result.UpcomingInvoice); |
||||
Assert.Equal(15.00m, result.UpcomingInvoice.Amount); |
||||
Assert.NotNull(result.UpcomingInvoice.Date); |
||||
|
||||
Assert.NotNull(result.CustomerDiscount); |
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id); |
||||
Assert.True(result.CustomerDiscount.Active); |
||||
Assert.Equal(20m, result.CustomerDiscount.PercentOff); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_WithNullSubscriptionAndInvoice_HandlesNullsGracefully( |
||||
User user, |
||||
UserLicense license) |
||||
{ |
||||
// Arrange - Test with null Subscription and UpcomingInvoice |
||||
var subscriptionInfo = new SubscriptionInfo |
||||
{ |
||||
Subscription = null, |
||||
UpcomingInvoice = null, |
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount |
||||
{ |
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, |
||||
Active = true, |
||||
PercentOff = 20m |
||||
} |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true); |
||||
|
||||
// Assert - Null Subscription and UpcomingInvoice should be handled gracefully |
||||
Assert.Null(result.Subscription); |
||||
Assert.Null(result.UpcomingInvoice); |
||||
Assert.NotNull(result.CustomerDiscount); |
||||
} |
||||
} |
||||
@ -0,0 +1,497 @@
@@ -0,0 +1,497 @@
|
||||
using Bit.Core.Models.Business; |
||||
using Bit.Test.Common.AutoFixture.Attributes; |
||||
using Stripe; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Core.Test.Models.Business; |
||||
|
||||
public class BillingCustomerDiscountTests |
||||
{ |
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_PercentageDiscount_SetsIdActivePercentOffAndAppliesTo(string couponId) |
||||
{ |
||||
// Arrange |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = 25.5m, |
||||
AmountOff = null, |
||||
AppliesTo = new CouponAppliesTo |
||||
{ |
||||
Products = new List<string> { "product1", "product2" } |
||||
} |
||||
}, |
||||
End = null // Active discount |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Equal(couponId, result.Id); |
||||
Assert.True(result.Active); |
||||
Assert.Equal(25.5m, result.PercentOff); |
||||
Assert.Null(result.AmountOff); |
||||
Assert.NotNull(result.AppliesTo); |
||||
Assert.Equal(2, result.AppliesTo.Count); |
||||
Assert.Contains("product1", result.AppliesTo); |
||||
Assert.Contains("product2", result.AppliesTo); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_AmountDiscount_ConvertsFromCentsToDollars(string couponId) |
||||
{ |
||||
// Arrange - Stripe sends 1400 cents for $14.00 |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = null, |
||||
AmountOff = 1400, // 1400 cents |
||||
AppliesTo = new CouponAppliesTo |
||||
{ |
||||
Products = new List<string>() |
||||
} |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Equal(couponId, result.Id); |
||||
Assert.True(result.Active); |
||||
Assert.Null(result.PercentOff); |
||||
Assert.Equal(14.00m, result.AmountOff); // Converted to dollars |
||||
Assert.NotNull(result.AppliesTo); |
||||
Assert.Empty(result.AppliesTo); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_InactiveDiscount_SetsActiveToFalse(string couponId) |
||||
{ |
||||
// Arrange |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = 15m |
||||
}, |
||||
End = DateTime.UtcNow.AddDays(-1) // Expired discount |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Equal(couponId, result.Id); |
||||
Assert.False(result.Active); |
||||
Assert.Equal(15m, result.PercentOff); |
||||
} |
||||
|
||||
[Fact] |
||||
public void Constructor_NullCoupon_SetsDiscountPropertiesToNull() |
||||
{ |
||||
// Arrange |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = null, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Null(result.Id); |
||||
Assert.True(result.Active); |
||||
Assert.Null(result.PercentOff); |
||||
Assert.Null(result.AmountOff); |
||||
Assert.Null(result.AppliesTo); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_NullAmountOff_SetsAmountOffToNull(string couponId) |
||||
{ |
||||
// Arrange |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = 10m, |
||||
AmountOff = null |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Null(result.AmountOff); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_ZeroAmountOff_ConvertsCorrectly(string couponId) |
||||
{ |
||||
// Arrange |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
AmountOff = 0 |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Equal(0m, result.AmountOff); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_LargeAmountOff_ConvertsCorrectly(string couponId) |
||||
{ |
||||
// Arrange - $100.00 discount |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
AmountOff = 10000 // 10000 cents = $100.00 |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Equal(100.00m, result.AmountOff); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_SmallAmountOff_ConvertsCorrectly(string couponId) |
||||
{ |
||||
// Arrange - $0.50 discount |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
AmountOff = 50 // 50 cents = $0.50 |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Equal(0.50m, result.AmountOff); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_BothDiscountTypes_SetsPercentOffAndAmountOff(string couponId) |
||||
{ |
||||
// Arrange - Coupon with both percentage and amount (edge case) |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = 20m, |
||||
AmountOff = 500 // $5.00 |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Equal(20m, result.PercentOff); |
||||
Assert.Equal(5.00m, result.AmountOff); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_WithNullAppliesTo_SetsAppliesToNull(string couponId) |
||||
{ |
||||
// Arrange |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = 10m, |
||||
AppliesTo = null |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Null(result.AppliesTo); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_WithNullProductsList_SetsAppliesToNull(string couponId) |
||||
{ |
||||
// Arrange |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = 10m, |
||||
AppliesTo = new CouponAppliesTo |
||||
{ |
||||
Products = null |
||||
} |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Null(result.AppliesTo); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_WithDecimalAmountOff_RoundsCorrectly(string couponId) |
||||
{ |
||||
// Arrange - 1425 cents = $14.25 |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
AmountOff = 1425 |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Equal(14.25m, result.AmountOff); |
||||
} |
||||
|
||||
[Fact] |
||||
public void Constructor_DefaultConstructor_InitializesAllPropertiesToNullOrFalse() |
||||
{ |
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(); |
||||
|
||||
// Assert |
||||
Assert.Null(result.Id); |
||||
Assert.False(result.Active); |
||||
Assert.Null(result.PercentOff); |
||||
Assert.Null(result.AmountOff); |
||||
Assert.Null(result.AppliesTo); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_WithFutureEndDate_SetsActiveToFalse(string couponId) |
||||
{ |
||||
// Arrange - Discount expires in the future |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = 20m |
||||
}, |
||||
End = DateTime.UtcNow.AddDays(30) // Expires in 30 days |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.False(result.Active); // Should be inactive because End is not null |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_WithPastEndDate_SetsActiveToFalse(string couponId) |
||||
{ |
||||
// Arrange - Discount already expired |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = 20m |
||||
}, |
||||
End = DateTime.UtcNow.AddDays(-30) // Expired 30 days ago |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.False(result.Active); // Should be inactive because End is not null |
||||
} |
||||
|
||||
[Fact] |
||||
public void Constructor_WithNullCouponId_SetsIdToNull() |
||||
{ |
||||
// Arrange |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = null, |
||||
PercentOff = 20m |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Null(result.Id); |
||||
Assert.True(result.Active); |
||||
Assert.Equal(20m, result.PercentOff); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_WithNullPercentOff_SetsPercentOffToNull(string couponId) |
||||
{ |
||||
// Arrange |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = null, |
||||
AmountOff = 1000 |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.Null(result.PercentOff); |
||||
Assert.Equal(10.00m, result.AmountOff); |
||||
} |
||||
|
||||
[Fact] |
||||
public void Constructor_WithCompleteStripeDiscount_MapsAllProperties() |
||||
{ |
||||
// Arrange - Comprehensive test with all Stripe Discount properties set |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = "premium_discount_2024", |
||||
PercentOff = 25m, |
||||
AmountOff = 1500, // $15.00 |
||||
AppliesTo = new CouponAppliesTo |
||||
{ |
||||
Products = new List<string> { "prod_premium", "prod_family", "prod_teams" } |
||||
} |
||||
}, |
||||
End = null // Active |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert - Verify all properties mapped correctly |
||||
Assert.Equal("premium_discount_2024", result.Id); |
||||
Assert.True(result.Active); |
||||
Assert.Equal(25m, result.PercentOff); |
||||
Assert.Equal(15.00m, result.AmountOff); |
||||
Assert.NotNull(result.AppliesTo); |
||||
Assert.Equal(3, result.AppliesTo.Count); |
||||
Assert.Contains("prod_premium", result.AppliesTo); |
||||
Assert.Contains("prod_family", result.AppliesTo); |
||||
Assert.Contains("prod_teams", result.AppliesTo); |
||||
} |
||||
|
||||
[Fact] |
||||
public void Constructor_WithMinimalStripeDiscount_HandlesNullsGracefully() |
||||
{ |
||||
// Arrange - Minimal Stripe Discount with most properties null |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = null, |
||||
PercentOff = null, |
||||
AmountOff = null, |
||||
AppliesTo = null |
||||
}, |
||||
End = DateTime.UtcNow.AddDays(10) // Has end date |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert - Should handle all nulls gracefully |
||||
Assert.Null(result.Id); |
||||
Assert.False(result.Active); |
||||
Assert.Null(result.PercentOff); |
||||
Assert.Null(result.AmountOff); |
||||
Assert.Null(result.AppliesTo); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData] |
||||
public void Constructor_WithEmptyProductsList_PreservesEmptyList(string couponId) |
||||
{ |
||||
// Arrange |
||||
var discount = new Discount |
||||
{ |
||||
Coupon = new Coupon |
||||
{ |
||||
Id = couponId, |
||||
PercentOff = 10m, |
||||
AppliesTo = new CouponAppliesTo |
||||
{ |
||||
Products = new List<string>() // Empty but not null |
||||
} |
||||
}, |
||||
End = null |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingCustomerDiscount(discount); |
||||
|
||||
// Assert |
||||
Assert.NotNull(result.AppliesTo); |
||||
Assert.Empty(result.AppliesTo); |
||||
} |
||||
} |
||||
@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
|
||||
using Bit.Core.Models.Business; |
||||
using Stripe; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Core.Test.Models.Business; |
||||
|
||||
public class SubscriptionInfoTests |
||||
{ |
||||
[Fact] |
||||
public void BillingSubscriptionItem_NullPlan_HandlesGracefully() |
||||
{ |
||||
// Arrange - SubscriptionItem with null Plan |
||||
var subscriptionItem = new SubscriptionItem |
||||
{ |
||||
Plan = null, |
||||
Quantity = 1 |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingSubscription.BillingSubscriptionItem(subscriptionItem); |
||||
|
||||
// Assert - Should handle null Plan gracefully |
||||
Assert.Null(result.ProductId); |
||||
Assert.Null(result.Name); |
||||
Assert.Equal(0m, result.Amount); // Defaults to 0 when Plan is null |
||||
Assert.Null(result.Interval); |
||||
Assert.Equal(1, result.Quantity); |
||||
Assert.False(result.SponsoredSubscriptionItem); |
||||
Assert.False(result.AddonSubscriptionItem); |
||||
} |
||||
|
||||
[Fact] |
||||
public void BillingSubscriptionItem_NullAmount_SetsToZero() |
||||
{ |
||||
// Arrange - SubscriptionItem with Plan but null Amount |
||||
var subscriptionItem = new SubscriptionItem |
||||
{ |
||||
Plan = new Plan |
||||
{ |
||||
ProductId = "prod_test", |
||||
Nickname = "Test Plan", |
||||
Amount = null, // Null amount |
||||
Interval = "month" |
||||
}, |
||||
Quantity = 1 |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingSubscription.BillingSubscriptionItem(subscriptionItem); |
||||
|
||||
// Assert - Should default to 0 when Amount is null |
||||
Assert.Equal("prod_test", result.ProductId); |
||||
Assert.Equal("Test Plan", result.Name); |
||||
Assert.Equal(0m, result.Amount); // Business rule: defaults to 0 when null |
||||
Assert.Equal("month", result.Interval); |
||||
Assert.Equal(1, result.Quantity); |
||||
} |
||||
|
||||
[Fact] |
||||
public void BillingSubscriptionItem_ZeroAmount_PreservesZero() |
||||
{ |
||||
// Arrange - SubscriptionItem with Plan and zero Amount |
||||
var subscriptionItem = new SubscriptionItem |
||||
{ |
||||
Plan = new Plan |
||||
{ |
||||
ProductId = "prod_test", |
||||
Nickname = "Test Plan", |
||||
Amount = 0, // Zero amount (0 cents) |
||||
Interval = "month" |
||||
}, |
||||
Quantity = 1 |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingSubscription.BillingSubscriptionItem(subscriptionItem); |
||||
|
||||
// Assert - Should preserve zero amount |
||||
Assert.Equal("prod_test", result.ProductId); |
||||
Assert.Equal("Test Plan", result.Name); |
||||
Assert.Equal(0m, result.Amount); // Zero amount preserved |
||||
Assert.Equal("month", result.Interval); |
||||
} |
||||
|
||||
[Fact] |
||||
public void BillingUpcomingInvoice_ZeroAmountDue_ConvertsToZero() |
||||
{ |
||||
// Arrange - Invoice with zero AmountDue |
||||
// Note: Stripe's Invoice.AmountDue is non-nullable long, so we test with 0 |
||||
// The null-coalescing operator (?? 0) in the constructor handles the case where |
||||
// ConvertFromStripeMinorUnits returns null, but since AmountDue is non-nullable, |
||||
// this test verifies the conversion path works correctly for zero values |
||||
var invoice = new Invoice |
||||
{ |
||||
AmountDue = 0, // Zero amount due (0 cents) |
||||
Created = DateTime.UtcNow |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingUpcomingInvoice(invoice); |
||||
|
||||
// Assert - Should convert zero correctly |
||||
Assert.Equal(0m, result.Amount); |
||||
Assert.NotNull(result.Date); |
||||
} |
||||
|
||||
[Fact] |
||||
public void BillingUpcomingInvoice_ValidAmountDue_ConvertsCorrectly() |
||||
{ |
||||
// Arrange - Invoice with valid AmountDue |
||||
var invoice = new Invoice |
||||
{ |
||||
AmountDue = 2500, // 2500 cents = $25.00 |
||||
Created = DateTime.UtcNow |
||||
}; |
||||
|
||||
// Act |
||||
var result = new SubscriptionInfo.BillingUpcomingInvoice(invoice); |
||||
|
||||
// Assert - Should convert correctly |
||||
Assert.Equal(25.00m, result.Amount); // Converted from cents |
||||
Assert.NotNull(result.Date); |
||||
} |
||||
} |
||||
|
||||
Loading…
Reference in new issue