mirror of
https://github.com/bitwarden/server
synced 2025-12-15 15:53:59 +00:00
[PM 26682]milestone 2d display discount on subscription page (#6542)
* 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 Test
This commit is contained in:
800
test/Api.Test/Billing/Controllers/AccountsControllerTests.cs
Normal file
800
test/Api.Test/Billing/Controllers/AccountsControllerTests.cs
Normal file
@@ -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>());
|
||||
}
|
||||
}
|
||||
400
test/Api.Test/Models/Response/SubscriptionResponseModelTests.cs
Normal file
400
test/Api.Test/Models/Response/SubscriptionResponseModelTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user