mirror of
https://github.com/bitwarden/server.git
synced 2025-07-15 22:57:44 -05:00
[PM-21881] Manage payment details outside of checkout (#6032)
* Add feature flag * Further establish billing command pattern and use in PreviewTaxAmountCommand * Add billing address models/commands/queries/tests * Update TypeReadingJsonConverter to account for new union types * Add payment method models/commands/queries/tests * Add credit models/commands/queries/tests * Add command/query registrations * Add new endpoints to support new command model and payment functionality * Run dotnet format * Add InjectUserAttribute for easier AccountBillilngVNextController handling * Add InjectOrganizationAttribute for easier OrganizationBillingVNextController handling * Add InjectProviderAttribute for easier ProviderBillingVNextController handling * Add XML documentation for billing command pipeline * Fix StripeConstants post-nullability * More nullability cleanup * Run dotnet format
This commit is contained in:
@ -0,0 +1,132 @@
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Billing.Attributes;
|
||||
|
||||
public class InjectOrganizationAttributeTests
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ActionExecutionDelegate _next;
|
||||
private readonly ActionExecutingContext _context;
|
||||
private readonly Organization _organization;
|
||||
private readonly Guid _organizationId;
|
||||
|
||||
public InjectOrganizationAttributeTests()
|
||||
{
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_organizationId = Guid.NewGuid();
|
||||
_organization = new Organization { Id = _organizationId };
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => _organizationRepository);
|
||||
httpContext.RequestServices = services.BuildServiceProvider();
|
||||
|
||||
var routeData = new RouteData { Values = { ["organizationId"] = _organizationId.ToString() } };
|
||||
|
||||
var actionContext = new ActionContext(
|
||||
httpContext,
|
||||
routeData,
|
||||
new ActionDescriptor(),
|
||||
new ModelStateDictionary()
|
||||
);
|
||||
|
||||
_next = () => Task.FromResult(new ActionExecutedContext(
|
||||
actionContext,
|
||||
new List<IFilterMetadata>(),
|
||||
new object()));
|
||||
|
||||
_context = new ActionExecutingContext(
|
||||
actionContext,
|
||||
new List<IFilterMetadata>(),
|
||||
new Dictionary<string, object>(),
|
||||
new object());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithExistingOrganization_InjectsOrganization()
|
||||
{
|
||||
var attribute = new InjectOrganizationAttribute();
|
||||
_organizationRepository.GetByIdAsync(_organizationId)
|
||||
.Returns(_organization);
|
||||
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "organization",
|
||||
ParameterType = typeof(Organization)
|
||||
};
|
||||
_context.ActionDescriptor.Parameters = [parameter];
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.Equal(_organization, _context.ActionArguments["organization"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithNonExistentOrganization_ReturnsNotFound()
|
||||
{
|
||||
var attribute = new InjectOrganizationAttribute();
|
||||
_organizationRepository.GetByIdAsync(_organizationId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.IsType<NotFoundObjectResult>(_context.Result);
|
||||
var result = (NotFoundObjectResult)_context.Result;
|
||||
Assert.IsType<ErrorResponseModel>(result.Value);
|
||||
Assert.Equal("Organization not found.", ((ErrorResponseModel)result.Value).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithInvalidOrganizationId_ReturnsBadRequest()
|
||||
{
|
||||
var attribute = new InjectOrganizationAttribute();
|
||||
_context.RouteData.Values["organizationId"] = "not-a-guid";
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.IsType<BadRequestObjectResult>(_context.Result);
|
||||
var result = (BadRequestObjectResult)_context.Result;
|
||||
Assert.IsType<ErrorResponseModel>(result.Value);
|
||||
Assert.Equal("Route parameter 'organizationId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithMissingOrganizationId_ReturnsBadRequest()
|
||||
{
|
||||
var attribute = new InjectOrganizationAttribute();
|
||||
_context.RouteData.Values.Clear();
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.IsType<BadRequestObjectResult>(_context.Result);
|
||||
var result = (BadRequestObjectResult)_context.Result;
|
||||
Assert.IsType<ErrorResponseModel>(result.Value);
|
||||
Assert.Equal("Route parameter 'organizationId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithoutOrganizationParameter_ContinuesExecution()
|
||||
{
|
||||
var attribute = new InjectOrganizationAttribute();
|
||||
_organizationRepository.GetByIdAsync(_organizationId)
|
||||
.Returns(_organization);
|
||||
|
||||
_context.ActionDescriptor.Parameters = Array.Empty<ParameterDescriptor>();
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.Empty(_context.ActionArguments);
|
||||
}
|
||||
}
|
190
test/Api.Test/Billing/Attributes/InjectProviderAttributeTests.cs
Normal file
190
test/Api.Test/Billing/Attributes/InjectProviderAttributeTests.cs
Normal file
@ -0,0 +1,190 @@
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Billing.Attributes;
|
||||
|
||||
public class InjectProviderAttributeTests
|
||||
{
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ActionExecutionDelegate _next;
|
||||
private readonly ActionExecutingContext _context;
|
||||
private readonly Provider _provider;
|
||||
private readonly Guid _providerId;
|
||||
|
||||
public InjectProviderAttributeTests()
|
||||
{
|
||||
_providerRepository = Substitute.For<IProviderRepository>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
_providerId = Guid.NewGuid();
|
||||
_provider = new Provider { Id = _providerId };
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => _providerRepository);
|
||||
services.AddScoped(_ => _currentContext);
|
||||
httpContext.RequestServices = services.BuildServiceProvider();
|
||||
|
||||
var routeData = new RouteData { Values = { ["providerId"] = _providerId.ToString() } };
|
||||
|
||||
var actionContext = new ActionContext(
|
||||
httpContext,
|
||||
routeData,
|
||||
new ActionDescriptor(),
|
||||
new ModelStateDictionary()
|
||||
);
|
||||
|
||||
_next = () => Task.FromResult(new ActionExecutedContext(
|
||||
actionContext,
|
||||
new List<IFilterMetadata>(),
|
||||
new object()));
|
||||
|
||||
_context = new ActionExecutingContext(
|
||||
actionContext,
|
||||
new List<IFilterMetadata>(),
|
||||
new Dictionary<string, object>(),
|
||||
new object());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithExistingProvider_InjectsProvider()
|
||||
{
|
||||
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
|
||||
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
|
||||
_currentContext.ProviderProviderAdmin(_providerId).Returns(true);
|
||||
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "provider",
|
||||
ParameterType = typeof(Provider)
|
||||
};
|
||||
_context.ActionDescriptor.Parameters = [parameter];
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.Equal(_provider, _context.ActionArguments["provider"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithNonExistentProvider_ReturnsNotFound()
|
||||
{
|
||||
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
|
||||
_providerRepository.GetByIdAsync(_providerId).Returns((Provider)null);
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.IsType<NotFoundObjectResult>(_context.Result);
|
||||
var result = (NotFoundObjectResult)_context.Result;
|
||||
Assert.IsType<ErrorResponseModel>(result.Value);
|
||||
Assert.Equal("Provider not found.", ((ErrorResponseModel)result.Value).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithInvalidProviderId_ReturnsBadRequest()
|
||||
{
|
||||
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
|
||||
_context.RouteData.Values["providerId"] = "not-a-guid";
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.IsType<BadRequestObjectResult>(_context.Result);
|
||||
var result = (BadRequestObjectResult)_context.Result;
|
||||
Assert.IsType<ErrorResponseModel>(result.Value);
|
||||
Assert.Equal("Route parameter 'providerId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithMissingProviderId_ReturnsBadRequest()
|
||||
{
|
||||
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
|
||||
_context.RouteData.Values.Clear();
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.IsType<BadRequestObjectResult>(_context.Result);
|
||||
var result = (BadRequestObjectResult)_context.Result;
|
||||
Assert.IsType<ErrorResponseModel>(result.Value);
|
||||
Assert.Equal("Route parameter 'providerId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithoutProviderParameter_ContinuesExecution()
|
||||
{
|
||||
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
|
||||
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
|
||||
_currentContext.ProviderProviderAdmin(_providerId).Returns(true);
|
||||
|
||||
_context.ActionDescriptor.Parameters = Array.Empty<ParameterDescriptor>();
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.Empty(_context.ActionArguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_UnauthorizedProviderAdmin_ReturnsUnauthorized()
|
||||
{
|
||||
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
|
||||
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
|
||||
_currentContext.ProviderProviderAdmin(_providerId).Returns(false);
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.IsType<UnauthorizedObjectResult>(_context.Result);
|
||||
var result = (UnauthorizedObjectResult)_context.Result;
|
||||
Assert.IsType<ErrorResponseModel>(result.Value);
|
||||
Assert.Equal("Unauthorized.", ((ErrorResponseModel)result.Value).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_UnauthorizedServiceUser_ReturnsUnauthorized()
|
||||
{
|
||||
var attribute = new InjectProviderAttribute(ProviderUserType.ServiceUser);
|
||||
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
|
||||
_currentContext.ProviderUser(_providerId).Returns(false);
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.IsType<UnauthorizedObjectResult>(_context.Result);
|
||||
var result = (UnauthorizedObjectResult)_context.Result;
|
||||
Assert.IsType<ErrorResponseModel>(result.Value);
|
||||
Assert.Equal("Unauthorized.", ((ErrorResponseModel)result.Value).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_AuthorizedProviderAdmin_Succeeds()
|
||||
{
|
||||
var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);
|
||||
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
|
||||
_currentContext.ProviderProviderAdmin(_providerId).Returns(true);
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.Null(_context.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_AuthorizedServiceUser_Succeeds()
|
||||
{
|
||||
var attribute = new InjectProviderAttribute(ProviderUserType.ServiceUser);
|
||||
_providerRepository.GetByIdAsync(_providerId).Returns(_provider);
|
||||
_currentContext.ProviderUser(_providerId).Returns(true);
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.Null(_context.Result);
|
||||
}
|
||||
}
|
129
test/Api.Test/Billing/Attributes/InjectUserAttributesTests.cs
Normal file
129
test/Api.Test/Billing/Attributes/InjectUserAttributesTests.cs
Normal file
@ -0,0 +1,129 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Billing.Attributes;
|
||||
|
||||
public class InjectUserAttributesTests
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly ActionExecutionDelegate _next;
|
||||
private readonly ActionExecutingContext _context;
|
||||
private readonly User _user;
|
||||
|
||||
public InjectUserAttributesTests()
|
||||
{
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_user = new User { Id = Guid.NewGuid() };
|
||||
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => _userService);
|
||||
httpContext.RequestServices = services.BuildServiceProvider();
|
||||
|
||||
var actionContext = new ActionContext(
|
||||
httpContext,
|
||||
new RouteData(),
|
||||
new ActionDescriptor(),
|
||||
new ModelStateDictionary()
|
||||
);
|
||||
|
||||
_next = () => Task.FromResult(new ActionExecutedContext(
|
||||
actionContext,
|
||||
new List<IFilterMetadata>(),
|
||||
new object()));
|
||||
|
||||
_context = new ActionExecutingContext(
|
||||
actionContext,
|
||||
new List<IFilterMetadata>(),
|
||||
new Dictionary<string, object>(),
|
||||
new object());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithAuthorizedUser_InjectsUser()
|
||||
{
|
||||
var attribute = new InjectUserAttribute();
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(_user);
|
||||
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "user",
|
||||
ParameterType = typeof(User)
|
||||
};
|
||||
_context.ActionDescriptor.Parameters = [parameter];
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.Equal(_user, _context.ActionArguments["user"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithUnauthorizedUser_ReturnsUnauthorized()
|
||||
{
|
||||
var attribute = new InjectUserAttribute();
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns((User)null);
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.IsType<UnauthorizedObjectResult>(_context.Result);
|
||||
var result = (UnauthorizedObjectResult)_context.Result;
|
||||
Assert.IsType<ErrorResponseModel>(result.Value);
|
||||
Assert.Equal("Unauthorized.", ((ErrorResponseModel)result.Value).Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithoutUserParameter_ContinuesExecution()
|
||||
{
|
||||
var attribute = new InjectUserAttribute();
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(_user);
|
||||
|
||||
_context.ActionDescriptor.Parameters = Array.Empty<ParameterDescriptor>();
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.Empty(_context.ActionArguments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnActionExecutionAsync_WithMultipleParameters_InjectsUserCorrectly()
|
||||
{
|
||||
var attribute = new InjectUserAttribute();
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(_user);
|
||||
|
||||
var parameters = new[]
|
||||
{
|
||||
new ParameterDescriptor
|
||||
{
|
||||
Name = "otherParam",
|
||||
ParameterType = typeof(string)
|
||||
},
|
||||
new ParameterDescriptor
|
||||
{
|
||||
Name = "user",
|
||||
ParameterType = typeof(User)
|
||||
}
|
||||
};
|
||||
_context.ActionDescriptor.Parameters = parameters;
|
||||
|
||||
await attribute.OnActionExecutionAsync(_context, _next);
|
||||
|
||||
Assert.Single(_context.ActionArguments);
|
||||
Assert.Equal(_user, _context.ActionArguments["user"]);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user