mirror of
https://github.com/bitwarden/server
synced 2026-01-01 16:13:33 +00:00
Merge branch 'master' into flexible-collections/deprecate-custom-collection-perm
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using System.Security.Claims;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
@@ -10,13 +12,17 @@ using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
@@ -145,4 +151,194 @@ public class OrganizationsControllerTests : IDisposable
|
||||
await _organizationService.DeleteUserAsync(orgId, user.Id);
|
||||
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException(
|
||||
Guid organizationId,
|
||||
OrganizationUpgradeRequestModel model)
|
||||
{
|
||||
_currentContext.EditSubscription(organizationId).Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostUpgrade(organizationId.ToString(), model));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task OrganizationsController_PostUpgrade_NonSMUpgrade_ReturnsCorrectResponse(
|
||||
Guid organizationId,
|
||||
OrganizationUpgradeRequestModel model,
|
||||
bool success,
|
||||
string paymentIntentClientSecret)
|
||||
{
|
||||
model.UseSecretsManager = false;
|
||||
|
||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||
|
||||
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||
|
||||
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
||||
|
||||
Assert.Equal(success, response.Success);
|
||||
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task OrganizationsController_PostUpgrade_SMUpgrade_ProvidesAccess_ReturnsCorrectResponse(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
OrganizationUpgradeRequestModel model,
|
||||
bool success,
|
||||
string paymentIntentClientSecret,
|
||||
OrganizationUser organizationUser)
|
||||
{
|
||||
model.UseSecretsManager = true;
|
||||
organizationUser.AccessSecretsManager = false;
|
||||
|
||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||
|
||||
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
|
||||
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).Returns(organizationUser);
|
||||
|
||||
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
||||
|
||||
Assert.Equal(success, response.Success);
|
||||
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||
|
||||
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
||||
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task OrganizationsController_PostUpgrade_SMUpgrade_NullOrgUser_ReturnsCorrectResponse(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
OrganizationUpgradeRequestModel model,
|
||||
bool success,
|
||||
string paymentIntentClientSecret)
|
||||
{
|
||||
model.UseSecretsManager = true;
|
||||
|
||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||
|
||||
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
|
||||
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).ReturnsNull();
|
||||
|
||||
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
||||
|
||||
Assert.Equal(success, response.Success);
|
||||
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||
|
||||
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrg_ThrowsNotFoundException(
|
||||
Guid organizationId,
|
||||
SecretsManagerSubscribeRequestModel model)
|
||||
{
|
||||
_organizationRepository.GetByIdAsync(organizationId).ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_UserCannotEditSubscription_ThrowsNotFoundException(
|
||||
Guid organizationId,
|
||||
SecretsManagerSubscribeRequestModel model,
|
||||
Organization organization)
|
||||
{
|
||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||
|
||||
_currentContext.EditSubscription(organizationId).Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_ProvidesAccess_ReturnsCorrectResponse(
|
||||
Guid organizationId,
|
||||
SecretsManagerSubscribeRequestModel model,
|
||||
Organization organization,
|
||||
Guid userId,
|
||||
OrganizationUser organizationUser,
|
||||
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
||||
{
|
||||
organizationUser.AccessSecretsManager = false;
|
||||
|
||||
var ssoConfigurationData = new SsoConfigurationData
|
||||
{
|
||||
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
||||
KeyConnectorUrl = "https://example.com"
|
||||
};
|
||||
|
||||
organizationUserOrganizationDetails.Permissions = string.Empty;
|
||||
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
||||
|
||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||
|
||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
|
||||
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).Returns(organizationUser);
|
||||
|
||||
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
||||
.Returns(organizationUserOrganizationDetails);
|
||||
|
||||
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
||||
|
||||
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
||||
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
||||
|
||||
await _addSecretsManagerSubscriptionCommand.Received(1)
|
||||
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
||||
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
||||
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrgUser_ReturnsCorrectResponse(
|
||||
Guid organizationId,
|
||||
SecretsManagerSubscribeRequestModel model,
|
||||
Organization organization,
|
||||
Guid userId,
|
||||
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
||||
{
|
||||
var ssoConfigurationData = new SsoConfigurationData
|
||||
{
|
||||
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
||||
KeyConnectorUrl = "https://example.com"
|
||||
};
|
||||
|
||||
organizationUserOrganizationDetails.Permissions = string.Empty;
|
||||
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
||||
|
||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||
|
||||
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
|
||||
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).ReturnsNull();
|
||||
|
||||
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
||||
.Returns(organizationUserOrganizationDetails);
|
||||
|
||||
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
||||
|
||||
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
||||
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
||||
|
||||
await _addSecretsManagerSubscriptionCommand.Received(1)
|
||||
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
||||
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Models.Request.Webauthn;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
@@ -22,7 +23,7 @@ public class WebAuthnControllerTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task Get_UserNotFound_ThrowsUnauthorizedAccessException(SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||
|
||||
// Act
|
||||
@@ -35,7 +36,7 @@ public class WebAuthnControllerTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||
|
||||
// Act
|
||||
@@ -59,10 +60,25 @@ public class WebAuthnControllerTests
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostOptions_RequireSsoPolicyApplicable_ThrowsBadRequestException(
|
||||
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(true);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.PostOptions(requestModel));
|
||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnCredentialRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||
|
||||
// Act
|
||||
@@ -113,10 +129,36 @@ public class WebAuthnControllerTests
|
||||
// Nothing to assert since return is void
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_RequireSsoPolicyApplicable_ThrowsBadRequestException(
|
||||
WebAuthnCredentialRequestModel requestModel,
|
||||
CredentialCreateOptions createOptions,
|
||||
User user,
|
||||
SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>())
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Unprotect(requestModel.Token)
|
||||
.Returns(token);
|
||||
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(true);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.Post(requestModel));
|
||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||
|
||||
// Act
|
||||
|
||||
@@ -133,8 +133,8 @@ public class SecretsManagerOrganizationCustomization : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
const PlanType planType = PlanType.EnterpriseAnnually;
|
||||
var organizationId = Guid.NewGuid();
|
||||
var planType = PlanType.EnterpriseAnnually;
|
||||
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.Id, organizationId)
|
||||
@@ -143,8 +143,7 @@ public class SecretsManagerOrganizationCustomization : ICustomization
|
||||
.With(o => o.PlanType, planType)
|
||||
.With(o => o.Plan, StaticStore.GetPlan(planType).Name)
|
||||
.With(o => o.MaxAutoscaleSmSeats, (int?)null)
|
||||
.With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null)
|
||||
);
|
||||
.With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,17 +15,45 @@ public class SecretsManagerSubscriptionUpdateTests
|
||||
[BitAutoData(PlanType.Custom)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||
public async Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
|
||||
public Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
|
||||
PlanType planType,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = planType;
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, false));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Invalid Secrets Manager plan", exception.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||
[BitAutoData(PlanType.TeamsMonthly)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||
[BitAutoData(PlanType.TeamsAnnually)]
|
||||
public void UpdateSubscription_WithNonSecretsManagerPlanType_DoesNotThrowException(
|
||||
PlanType planType,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = planType;
|
||||
|
||||
// Act
|
||||
var ex = Record.Exception(() => new SecretsManagerSubscriptionUpdate(organization, false));
|
||||
|
||||
// Assert
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,4 +739,300 @@ public class StripePaymentServiceTests
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PreviewUpcomingInvoiceAndPayAsync_WithInAppPaymentMethod_ThrowsBadRequestException(SutProvider<StripePaymentService> sutProvider,
|
||||
Organization subscriber, List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions)
|
||||
{
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerGetAsync(Arg.Any<string>(), Arg.Any<Stripe.CustomerGetOptions>())
|
||||
.Returns(new Stripe.Customer { Metadata = new Dictionary<string, string> { { "appleReceipt", "dummyData" } } });
|
||||
|
||||
var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, subItemOptions));
|
||||
Assert.Equal("Cannot perform this action with in-app purchase payment method. Contact support.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void PreviewUpcomingInvoiceAndPayAsync_UpcomingInvoiceBelowThreshold_DoesNotInvoiceNow(SutProvider<StripePaymentService> sutProvider,
|
||||
Organization subscriber, List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions)
|
||||
{
|
||||
var prorateThreshold = 50000;
|
||||
var invoiceAmountBelowThreshold = prorateThreshold - 100;
|
||||
var customer = MockStripeCustomer(subscriber);
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(default, default).ReturnsForAnyArgs(customer);
|
||||
var invoiceItem = MockInoviceItemList(subscriber, "planId", invoiceAmountBelowThreshold, customer);
|
||||
sutProvider.GetDependency<IStripeAdapter>().InvoiceItemListAsync(new Stripe.InvoiceItemListOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId
|
||||
}).ReturnsForAnyArgs(invoiceItem);
|
||||
|
||||
var invoiceLineItem = CreateInvoiceLineTime(subscriber, "planId", invoiceAmountBelowThreshold);
|
||||
sutProvider.GetDependency<IStripeAdapter>().InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId,
|
||||
Subscription = subscriber.GatewaySubscriptionId,
|
||||
SubscriptionItems = subItemOptions
|
||||
}).ReturnsForAnyArgs(invoiceLineItem);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().InvoiceCreateAsync(Arg.Is<Stripe.InvoiceCreateOptions>(options =>
|
||||
options.CollectionMethod == "send_invoice" &&
|
||||
options.DaysUntilDue == 1 &&
|
||||
options.Customer == subscriber.GatewayCustomerId &&
|
||||
options.Subscription == subscriber.GatewaySubscriptionId &&
|
||||
options.DefaultPaymentMethod == customer.InvoiceSettings.DefaultPaymentMethod.Id
|
||||
)).ReturnsForAnyArgs(new Stripe.Invoice
|
||||
{
|
||||
Id = "mockInvoiceId",
|
||||
CollectionMethod = "send_invoice",
|
||||
DueDate = DateTime.Now.AddDays(1),
|
||||
Customer = customer,
|
||||
Subscription = new Stripe.Subscription
|
||||
{
|
||||
Id = "mockSubscriptionId",
|
||||
Customer = customer,
|
||||
Status = "active",
|
||||
CurrentPeriodStart = DateTime.UtcNow,
|
||||
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1),
|
||||
CollectionMethod = "charge_automatically",
|
||||
},
|
||||
DefaultPaymentMethod = customer.InvoiceSettings.DefaultPaymentMethod,
|
||||
AmountDue = invoiceAmountBelowThreshold,
|
||||
Currency = "usd",
|
||||
Status = "draft",
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, new List<Stripe.InvoiceSubscriptionItemOptions>(), prorateThreshold);
|
||||
|
||||
Assert.False(result.IsInvoicedNow);
|
||||
Assert.Null(result.PaymentIntentClientSecret);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void PreviewUpcomingInvoiceAndPayAsync_NoPaymentMethod_ThrowsBadRequestException(SutProvider<StripePaymentService> sutProvider,
|
||||
Organization subscriber, List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions, string planId)
|
||||
{
|
||||
var prorateThreshold = 120000;
|
||||
var invoiceAmountBelowThreshold = prorateThreshold;
|
||||
var customer = new Stripe.Customer
|
||||
{
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
Id = subscriber.GatewayCustomerId,
|
||||
DefaultSource = null,
|
||||
InvoiceSettings = new Stripe.CustomerInvoiceSettings
|
||||
{
|
||||
DefaultPaymentMethod = null
|
||||
}
|
||||
};
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(default, default).ReturnsForAnyArgs(customer);
|
||||
var invoiceItem = MockInoviceItemList(subscriber, planId, invoiceAmountBelowThreshold, customer);
|
||||
sutProvider.GetDependency<IStripeAdapter>().InvoiceItemListAsync(new Stripe.InvoiceItemListOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId
|
||||
}).ReturnsForAnyArgs(invoiceItem);
|
||||
|
||||
var invoiceLineItem = CreateInvoiceLineTime(subscriber, planId, invoiceAmountBelowThreshold);
|
||||
sutProvider.GetDependency<IStripeAdapter>().InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId,
|
||||
Subscription = subscriber.GatewaySubscriptionId,
|
||||
SubscriptionItems = subItemOptions
|
||||
}).ReturnsForAnyArgs(invoiceLineItem);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, subItemOptions));
|
||||
Assert.Equal("No payment method is available.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void PreviewUpcomingInvoiceAndPayAsync_UpcomingInvoiceAboveThreshold_DoesInvoiceNow(SutProvider<StripePaymentService> sutProvider,
|
||||
Organization subscriber, List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions, string planId)
|
||||
{
|
||||
var prorateThreshold = 50000;
|
||||
var invoiceAmountBelowThreshold = 100000;
|
||||
var customer = MockStripeCustomer(subscriber);
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(default, default).ReturnsForAnyArgs(customer);
|
||||
var invoiceItem = MockInoviceItemList(subscriber, planId, invoiceAmountBelowThreshold, customer);
|
||||
sutProvider.GetDependency<IStripeAdapter>().InvoiceItemListAsync(new Stripe.InvoiceItemListOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId
|
||||
}).ReturnsForAnyArgs(invoiceItem);
|
||||
|
||||
var invoiceLineItem = CreateInvoiceLineTime(subscriber, planId, invoiceAmountBelowThreshold);
|
||||
sutProvider.GetDependency<IStripeAdapter>().InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId,
|
||||
Subscription = subscriber.GatewaySubscriptionId,
|
||||
SubscriptionItems = subItemOptions
|
||||
}).ReturnsForAnyArgs(invoiceLineItem);
|
||||
|
||||
var invoice = MockInVoice(customer, invoiceAmountBelowThreshold);
|
||||
sutProvider.GetDependency<IStripeAdapter>().InvoiceCreateAsync(Arg.Is<Stripe.InvoiceCreateOptions>(options =>
|
||||
options.CollectionMethod == "send_invoice" &&
|
||||
options.DaysUntilDue == 1 &&
|
||||
options.Customer == subscriber.GatewayCustomerId &&
|
||||
options.Subscription == subscriber.GatewaySubscriptionId &&
|
||||
options.DefaultPaymentMethod == customer.InvoiceSettings.DefaultPaymentMethod.Id
|
||||
)).ReturnsForAnyArgs(invoice);
|
||||
|
||||
var result = await sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, new List<Stripe.InvoiceSubscriptionItemOptions>(), prorateThreshold);
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).InvoicePayAsync(invoice.Id,
|
||||
Arg.Is<Stripe.InvoicePayOptions>((options =>
|
||||
options.OffSession == true
|
||||
)));
|
||||
|
||||
|
||||
Assert.True(result.IsInvoicedNow);
|
||||
Assert.Null(result.PaymentIntentClientSecret);
|
||||
}
|
||||
|
||||
private static Stripe.Invoice MockInVoice(Stripe.Customer customer, int invoiceAmountBelowThreshold) =>
|
||||
new()
|
||||
{
|
||||
Id = "mockInvoiceId",
|
||||
CollectionMethod = "send_invoice",
|
||||
DueDate = DateTime.Now.AddDays(1),
|
||||
Customer = customer,
|
||||
Subscription = new Stripe.Subscription
|
||||
{
|
||||
Id = "mockSubscriptionId",
|
||||
Customer = customer,
|
||||
Status = "active",
|
||||
CurrentPeriodStart = DateTime.UtcNow,
|
||||
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1),
|
||||
CollectionMethod = "charge_automatically",
|
||||
},
|
||||
DefaultPaymentMethod = customer.InvoiceSettings.DefaultPaymentMethod,
|
||||
AmountDue = invoiceAmountBelowThreshold,
|
||||
Currency = "usd",
|
||||
Status = "draft",
|
||||
};
|
||||
|
||||
private static List<Stripe.InvoiceItem> MockInoviceItemList(Organization subscriber, string planId, int invoiceAmountBelowThreshold, Stripe.Customer customer) =>
|
||||
new()
|
||||
{
|
||||
new Stripe.InvoiceItem
|
||||
{
|
||||
Id = "ii_1234567890",
|
||||
Amount = invoiceAmountBelowThreshold,
|
||||
Currency = "usd",
|
||||
CustomerId = subscriber.GatewayCustomerId,
|
||||
Description = "Sample invoice item 1",
|
||||
Date = DateTime.UtcNow,
|
||||
Discountable = true,
|
||||
InvoiceId = "548458365"
|
||||
},
|
||||
new Stripe.InvoiceItem
|
||||
{
|
||||
Id = "ii_0987654321",
|
||||
Amount = invoiceAmountBelowThreshold,
|
||||
Currency = "usd",
|
||||
CustomerId = customer.Id,
|
||||
Description = "Sample invoice item 2",
|
||||
Date = DateTime.UtcNow.AddDays(-5),
|
||||
Discountable = false,
|
||||
InvoiceId = null,
|
||||
Proration = true,
|
||||
Plan = new Stripe.Plan
|
||||
{
|
||||
Id = planId,
|
||||
Amount = invoiceAmountBelowThreshold,
|
||||
Currency = "usd",
|
||||
Interval = "month",
|
||||
IntervalCount = 1,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
private static Stripe.Customer MockStripeCustomer(Organization subscriber)
|
||||
{
|
||||
var customer = new Stripe.Customer
|
||||
{
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
Id = subscriber.GatewayCustomerId,
|
||||
DefaultSource = new Stripe.Card
|
||||
{
|
||||
Id = "card_12345",
|
||||
Last4 = "1234",
|
||||
Brand = "Visa",
|
||||
ExpYear = 2025,
|
||||
ExpMonth = 12
|
||||
},
|
||||
InvoiceSettings = new Stripe.CustomerInvoiceSettings
|
||||
{
|
||||
DefaultPaymentMethod = new Stripe.PaymentMethod
|
||||
{
|
||||
Id = "pm_12345",
|
||||
Type = "card",
|
||||
Card = new Stripe.PaymentMethodCard
|
||||
{
|
||||
Last4 = "1234",
|
||||
Brand = "Visa",
|
||||
ExpYear = 2025,
|
||||
ExpMonth = 12
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return customer;
|
||||
}
|
||||
|
||||
private static Stripe.Invoice CreateInvoiceLineTime(Organization subscriber, string planId, int invoiceAmountBelowThreshold) =>
|
||||
new()
|
||||
{
|
||||
AmountDue = invoiceAmountBelowThreshold,
|
||||
AmountPaid = 0,
|
||||
AmountRemaining = invoiceAmountBelowThreshold,
|
||||
CustomerId = subscriber.GatewayCustomerId,
|
||||
SubscriptionId = subscriber.GatewaySubscriptionId,
|
||||
ApplicationFeeAmount = 0,
|
||||
Currency = "usd",
|
||||
Description = "Upcoming Invoice",
|
||||
Discount = null,
|
||||
DueDate = DateTime.UtcNow.AddDays(1),
|
||||
EndingBalance = 0,
|
||||
Number = "INV12345",
|
||||
Paid = false,
|
||||
PeriodStart = DateTime.UtcNow,
|
||||
PeriodEnd = DateTime.UtcNow.AddMonths(1),
|
||||
ReceiptNumber = null,
|
||||
StartingBalance = 0,
|
||||
Status = "draft",
|
||||
Id = "ii_0987654321",
|
||||
Total = invoiceAmountBelowThreshold,
|
||||
Lines = new Stripe.StripeList<Stripe.InvoiceLineItem>
|
||||
{
|
||||
Data = new List<Stripe.InvoiceLineItem>
|
||||
{
|
||||
new Stripe.InvoiceLineItem
|
||||
{
|
||||
Amount = invoiceAmountBelowThreshold,
|
||||
Currency = "usd",
|
||||
Description = "Sample line item",
|
||||
Id = "ii_0987654321",
|
||||
Livemode = false,
|
||||
Object = "line_item",
|
||||
Discountable = false,
|
||||
Period = new Stripe.InvoiceLineItemPeriod()
|
||||
{
|
||||
Start = DateTime.UtcNow,
|
||||
End = DateTime.UtcNow.AddMonths(1)
|
||||
},
|
||||
Plan = new Stripe.Plan
|
||||
{
|
||||
Id = planId,
|
||||
Amount = invoiceAmountBelowThreshold,
|
||||
Currency = "usd",
|
||||
Interval = "month",
|
||||
IntervalCount = 1,
|
||||
},
|
||||
Proration = true,
|
||||
Quantity = 1,
|
||||
Subscription = subscriber.GatewaySubscriptionId,
|
||||
SubscriptionItem = "si_12345",
|
||||
Type = "subscription",
|
||||
UnitAmountExcludingTax = invoiceAmountBelowThreshold,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ public class StaticStoreTests
|
||||
[Fact]
|
||||
public void StaticStore_Initialization_Success()
|
||||
{
|
||||
var plans = StaticStore.Plans;
|
||||
var plans = StaticStore.Plans.ToList();
|
||||
Assert.NotNull(plans);
|
||||
Assert.NotEmpty(plans);
|
||||
Assert.Equal(12, plans.Count());
|
||||
Assert.Equal(16, plans.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
Reference in New Issue
Block a user