mirror of
https://github.com/bitwarden/server
synced 2026-01-02 00:23:40 +00:00
Merge branch 'main' into auth/pm-22975/client-version-validator
This commit is contained in:
@@ -3,9 +3,11 @@ using System.Net;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Bit.Api.KeyManagement.Models.Responses;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
@@ -286,20 +288,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier,
|
||||
SetKeyConnectorKeyRequestModel request)
|
||||
{
|
||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||
PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,
|
||||
paymentMethod: PaymentMethodType.Card);
|
||||
organization.UseKeyConnector = true;
|
||||
organization.UseSso = true;
|
||||
organization.Identifier = organizationSsoIdentifier;
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
var ssoUserEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(ssoUserEmail);
|
||||
await _loginHelper.LoginAsync(ssoUserEmail);
|
||||
|
||||
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail,
|
||||
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Invited);
|
||||
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);
|
||||
|
||||
var ssoUser = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||
Assert.NotNull(ssoUser);
|
||||
@@ -340,19 +329,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
[Fact]
|
||||
public async Task PostConvertToKeyConnectorAsync_Success()
|
||||
{
|
||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||
PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,
|
||||
paymentMethod: PaymentMethodType.Card);
|
||||
organization.UseKeyConnector = true;
|
||||
organization.UseSso = true;
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
var ssoUserEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(ssoUserEmail);
|
||||
await _loginHelper.LoginAsync(ssoUserEmail);
|
||||
|
||||
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail,
|
||||
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted);
|
||||
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Accepted);
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/accounts/convert-to-key-connector", new { });
|
||||
response.EnsureSuccessStatusCode();
|
||||
@@ -556,4 +533,41 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetKeyConnectorConfirmationDetailsAsync_Success()
|
||||
{
|
||||
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited);
|
||||
|
||||
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail,
|
||||
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted);
|
||||
|
||||
var response = await _client.GetAsync($"/accounts/key-connector/confirmation-details/{organization.Identifier}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<KeyConnectorConfirmationDetailsResponseModel>();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organization.Name, result.OrganizationName);
|
||||
}
|
||||
|
||||
private async Task<(string, Organization)> SetupKeyConnectorTestAsync(OrganizationUserStatusType userStatusType,
|
||||
string organizationSsoIdentifier = "test-sso-identifier")
|
||||
{
|
||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||
PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,
|
||||
paymentMethod: PaymentMethodType.Card);
|
||||
organization.UseKeyConnector = true;
|
||||
organization.UseSso = true;
|
||||
organization.Identifier = organizationSsoIdentifier;
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
var ssoUserEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(ssoUserEmail);
|
||||
await _loginHelper.LoginAsync(ssoUserEmail);
|
||||
|
||||
await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail,
|
||||
OrganizationUserType.User, userStatusType: userStatusType);
|
||||
|
||||
return (ssoUserEmail, organization);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Api.Billing.Controllers;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@@ -11,8 +10,6 @@ using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
@@ -521,49 +518,4 @@ public class ProviderBillingControllerTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateTaxInformationAsync
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateTaxInformation_NoCountry_BadRequest(
|
||||
Provider provider,
|
||||
TaxInformationRequestBody requestBody,
|
||||
SutProvider<ProviderBillingController> sutProvider)
|
||||
{
|
||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
||||
|
||||
requestBody.Country = null;
|
||||
|
||||
var result = await sutProvider.Sut.UpdateTaxInformationAsync(provider.Id, requestBody);
|
||||
|
||||
Assert.IsType<BadRequest<ErrorResponseModel>>(result);
|
||||
|
||||
var response = (BadRequest<ErrorResponseModel>)result;
|
||||
|
||||
Assert.Equal("Country and postal code are required to update your tax information.", response.Value.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateTaxInformation_Ok(
|
||||
Provider provider,
|
||||
TaxInformationRequestBody requestBody,
|
||||
SutProvider<ProviderBillingController> sutProvider)
|
||||
{
|
||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
||||
|
||||
await sutProvider.Sut.UpdateTaxInformationAsync(provider.Id, requestBody);
|
||||
|
||||
await sutProvider.GetDependency<ISubscriberService>().Received(1).UpdateTaxInformation(
|
||||
provider, Arg.Is<TaxInformation>(
|
||||
options =>
|
||||
options.Country == requestBody.Country &&
|
||||
options.PostalCode == requestBody.PostalCode &&
|
||||
options.TaxId == requestBody.TaxId &&
|
||||
options.Line1 == requestBody.Line1 &&
|
||||
options.Line2 == requestBody.Line2 &&
|
||||
options.City == requestBody.City &&
|
||||
options.State == requestBody.State));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -363,4 +364,39 @@ public class AccountsKeyManagementControllerTests
|
||||
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetKeyConnectorConfirmationDetailsAsync_NoUser_Throws(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider, string orgSsoIdentifier)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
|
||||
sutProvider.Sut.GetKeyConnectorConfirmationDetailsAsync(orgSsoIdentifier));
|
||||
|
||||
await sutProvider.GetDependency<IKeyConnectorConfirmationDetailsQuery>().ReceivedWithAnyArgs(0)
|
||||
.Run(Arg.Any<string>(), Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetKeyConnectorConfirmationDetailsAsync_Success(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider, User expectedUser, string orgSsoIdentifier)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(expectedUser);
|
||||
sutProvider.GetDependency<IKeyConnectorConfirmationDetailsQuery>().Run(orgSsoIdentifier, expectedUser.Id)
|
||||
.Returns(
|
||||
new KeyConnectorConfirmationDetails { OrganizationName = "test" }
|
||||
);
|
||||
|
||||
var result = await sutProvider.Sut.GetKeyConnectorConfirmationDetailsAsync(orgSsoIdentifier);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("test", result.OrganizationName);
|
||||
await sutProvider.GetDependency<IKeyConnectorConfirmationDetailsQuery>().Received(1)
|
||||
.Run(orgSsoIdentifier, expectedUser.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1909,4 +1909,237 @@ public class CiphersControllerTests
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostPurge(model, organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShare_WithNullFolderAndFalseFavorite_UpdatesFieldsCorrectly(
|
||||
Guid cipherId,
|
||||
Guid userId,
|
||||
Guid organizationId,
|
||||
Guid folderId,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var user = new User { Id = userId };
|
||||
var userIdKey = userId.ToString().ToUpperInvariant();
|
||||
|
||||
var existingCipher = new Cipher
|
||||
{
|
||||
Id = cipherId,
|
||||
UserId = userId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, folderId.ToString().ToUpperInvariant() } }),
|
||||
Favorites = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, true } })
|
||||
};
|
||||
|
||||
// Clears folder and favorite when sharing
|
||||
var model = new CipherShareRequestModel
|
||||
{
|
||||
Cipher = new CipherRequestModel
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organizationId.ToString(),
|
||||
Name = "SharedCipher",
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
FolderId = null,
|
||||
Favorite = false,
|
||||
EncryptedFor = userId
|
||||
},
|
||||
CollectionIds = [Guid.NewGuid().ToString()]
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId)
|
||||
.Returns(existingCipher);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(true);
|
||||
|
||||
var sharedCipher = new CipherDetails
|
||||
{
|
||||
Id = cipherId,
|
||||
OrganizationId = organizationId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
FolderId = null,
|
||||
Favorite = false
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId, userId)
|
||||
.Returns(sharedCipher);
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilitiesAsync()
|
||||
.Returns(new Dictionary<Guid, OrganizationAbility>
|
||||
{
|
||||
{ organizationId, new OrganizationAbility { Id = organizationId } }
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.PutShare(cipherId, model);
|
||||
|
||||
Assert.Null(result.FolderId);
|
||||
Assert.False(result.Favorite);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShare_WithFolderAndFavoriteSet_AddsUserSpecificFields(
|
||||
Guid cipherId,
|
||||
Guid userId,
|
||||
Guid organizationId,
|
||||
Guid folderId,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var user = new User { Id = userId };
|
||||
var userIdKey = userId.ToString().ToUpperInvariant();
|
||||
|
||||
var existingCipher = new Cipher
|
||||
{
|
||||
Id = cipherId,
|
||||
UserId = userId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
Folders = null,
|
||||
Favorites = null
|
||||
};
|
||||
|
||||
// Sets folder and favorite when sharing
|
||||
var model = new CipherShareRequestModel
|
||||
{
|
||||
Cipher = new CipherRequestModel
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organizationId.ToString(),
|
||||
Name = "SharedCipher",
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
FolderId = folderId.ToString(),
|
||||
Favorite = true,
|
||||
EncryptedFor = userId
|
||||
},
|
||||
CollectionIds = [Guid.NewGuid().ToString()]
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId)
|
||||
.Returns(existingCipher);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(true);
|
||||
|
||||
var sharedCipher = new CipherDetails
|
||||
{
|
||||
Id = cipherId,
|
||||
OrganizationId = organizationId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, folderId.ToString().ToUpperInvariant() } }),
|
||||
Favorites = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, true } }),
|
||||
FolderId = folderId,
|
||||
Favorite = true
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId, userId)
|
||||
.Returns(sharedCipher);
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilitiesAsync()
|
||||
.Returns(new Dictionary<Guid, OrganizationAbility>
|
||||
{
|
||||
{ organizationId, new OrganizationAbility { Id = organizationId } }
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.PutShare(cipherId, model);
|
||||
|
||||
Assert.Equal(folderId, result.FolderId);
|
||||
Assert.True(result.Favorite);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShare_UpdateExistingFolderAndFavorite_UpdatesUserSpecificFields(
|
||||
Guid cipherId,
|
||||
Guid userId,
|
||||
Guid organizationId,
|
||||
Guid oldFolderId,
|
||||
Guid newFolderId,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
var user = new User { Id = userId };
|
||||
var userIdKey = userId.ToString().ToUpperInvariant();
|
||||
|
||||
// Existing cipher with old folder and not favorited
|
||||
var existingCipher = new Cipher
|
||||
{
|
||||
Id = cipherId,
|
||||
UserId = userId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, oldFolderId.ToString().ToUpperInvariant() } }),
|
||||
Favorites = null
|
||||
};
|
||||
|
||||
var model = new CipherShareRequestModel
|
||||
{
|
||||
Cipher = new CipherRequestModel
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = organizationId.ToString(),
|
||||
Name = "SharedCipher",
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
FolderId = newFolderId.ToString(), // Update to new folder
|
||||
Favorite = true, // Add favorite
|
||||
EncryptedFor = userId
|
||||
},
|
||||
CollectionIds = [Guid.NewGuid().ToString()]
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId)
|
||||
.Returns(existingCipher);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(true);
|
||||
|
||||
var sharedCipher = new CipherDetails
|
||||
{
|
||||
Id = cipherId,
|
||||
OrganizationId = organizationId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new { Username = "test", Password = "test" }),
|
||||
Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, newFolderId.ToString().ToUpperInvariant() } }),
|
||||
Favorites = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, true } }),
|
||||
FolderId = newFolderId,
|
||||
Favorite = true
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId, userId)
|
||||
.Returns(sharedCipher);
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>()
|
||||
.GetOrganizationAbilitiesAsync()
|
||||
.Returns(new Dictionary<Guid, OrganizationAbility>
|
||||
{
|
||||
{ organizationId, new OrganizationAbility { Id = organizationId } }
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.PutShare(cipherId, model);
|
||||
|
||||
Assert.Equal(newFolderId, result.FolderId);
|
||||
Assert.True(result.Favorite);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Divergic.Logging.Xunit" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="Neovolve.Logging.Xunit" Version="6.3.0" />
|
||||
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||
|
||||
@@ -8,13 +8,13 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Divergic.Logging.Xunit;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Neovolve.Logging.Xunit;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
@@ -23,10 +23,8 @@ using Transaction = Bit.Core.Entities.Transaction;
|
||||
|
||||
namespace Bit.Billing.Test.Controllers;
|
||||
|
||||
public class PayPalControllerTests
|
||||
public class PayPalControllerTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
private readonly ITestOutputHelper _testOutputHelper;
|
||||
|
||||
private readonly IOptions<BillingSettings> _billingSettings = Substitute.For<IOptions<BillingSettings>>();
|
||||
private readonly IMailService _mailService = Substitute.For<IMailService>();
|
||||
private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
@@ -38,15 +36,10 @@ public class PayPalControllerTests
|
||||
|
||||
private const string _defaultWebhookKey = "webhook-key";
|
||||
|
||||
public PayPalControllerTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_testOutputHelper = testOutputHelper;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostIpn_NullKey_BadRequest()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
var controller = ConfigureControllerContextWith(logger, null, null);
|
||||
|
||||
@@ -60,7 +53,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_IncorrectKey_BadRequest()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -79,7 +72,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_EmptyIPNBody_BadRequest()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -98,7 +91,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_IPNHasNoEntityId_BadRequest()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -119,15 +112,13 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_OtherTransactionType_Unprocessed_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
PayPal = { WebhookKey = _defaultWebhookKey }
|
||||
});
|
||||
|
||||
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e");
|
||||
|
||||
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.UnsupportedTransactionType);
|
||||
|
||||
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);
|
||||
@@ -142,7 +133,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_MismatchedReceiverID_Unprocessed_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -153,8 +144,6 @@ public class PayPalControllerTests
|
||||
}
|
||||
});
|
||||
|
||||
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e");
|
||||
|
||||
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment);
|
||||
|
||||
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);
|
||||
@@ -169,7 +158,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_RefundMissingParent_Unprocessed_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -180,8 +169,6 @@ public class PayPalControllerTests
|
||||
}
|
||||
});
|
||||
|
||||
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e");
|
||||
|
||||
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.RefundMissingParentTransaction);
|
||||
|
||||
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);
|
||||
@@ -196,7 +183,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_eCheckPayment_Unprocessed_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -207,8 +194,6 @@ public class PayPalControllerTests
|
||||
}
|
||||
});
|
||||
|
||||
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e");
|
||||
|
||||
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.ECheckPayment);
|
||||
|
||||
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);
|
||||
@@ -223,7 +208,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_NonUSD_Unprocessed_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -234,8 +219,6 @@ public class PayPalControllerTests
|
||||
}
|
||||
});
|
||||
|
||||
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e");
|
||||
|
||||
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.NonUSDPayment);
|
||||
|
||||
var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);
|
||||
@@ -250,7 +233,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_Completed_ExistingTransaction_Unprocessed_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -261,8 +244,6 @@ public class PayPalControllerTests
|
||||
}
|
||||
});
|
||||
|
||||
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e");
|
||||
|
||||
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment);
|
||||
|
||||
_transactionRepository.GetByGatewayIdAsync(
|
||||
@@ -281,7 +262,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_Completed_CreatesTransaction_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -292,8 +273,6 @@ public class PayPalControllerTests
|
||||
}
|
||||
});
|
||||
|
||||
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e");
|
||||
|
||||
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment);
|
||||
|
||||
_transactionRepository.GetByGatewayIdAsync(
|
||||
@@ -314,7 +293,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_Completed_CreatesTransaction_CreditsOrganizationAccount_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -362,7 +341,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_Completed_CreatesTransaction_CreditsUserAccount_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -406,7 +385,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_Refunded_ExistingTransaction_Unprocessed_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -417,8 +396,6 @@ public class PayPalControllerTests
|
||||
}
|
||||
});
|
||||
|
||||
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e");
|
||||
|
||||
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulRefund);
|
||||
|
||||
_transactionRepository.GetByGatewayIdAsync(
|
||||
@@ -441,7 +418,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_Refunded_MissingParentTransaction_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -452,8 +429,6 @@ public class PayPalControllerTests
|
||||
}
|
||||
});
|
||||
|
||||
var organizationId = new Guid("ca8c6f2b-2d7b-4639-809f-b0e5013a304e");
|
||||
|
||||
var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulRefund);
|
||||
|
||||
_transactionRepository.GetByGatewayIdAsync(
|
||||
@@ -480,7 +455,7 @@ public class PayPalControllerTests
|
||||
[Fact]
|
||||
public async Task PostIpn_Refunded_ReplacesParent_CreatesTransaction_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
var logger = testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
_billingSettings.Value.Returns(new BillingSettings
|
||||
{
|
||||
@@ -531,8 +506,8 @@ public class PayPalControllerTests
|
||||
|
||||
private PayPalController ConfigureControllerContextWith(
|
||||
ILogger<PayPalController> logger,
|
||||
string webhookKey,
|
||||
string ipnBody)
|
||||
string? webhookKey,
|
||||
string? ipnBody)
|
||||
{
|
||||
var controller = new PayPalController(
|
||||
_billingSettings,
|
||||
@@ -578,16 +553,16 @@ public class PayPalControllerTests
|
||||
Assert.Equal(statusCode, statusCodeActionResult.StatusCode);
|
||||
}
|
||||
|
||||
private static void Logged(ICacheLogger logger, LogLevel logLevel, string message)
|
||||
private static void Logged(ICacheLogger<PayPalController> logger, LogLevel logLevel, string message)
|
||||
{
|
||||
Assert.NotNull(logger.Last);
|
||||
Assert.Equal(logLevel, logger.Last!.LogLevel);
|
||||
Assert.Equal(message, logger.Last!.Message);
|
||||
}
|
||||
|
||||
private static void LoggedError(ICacheLogger logger, string message)
|
||||
private static void LoggedError(ICacheLogger<PayPalController> logger, string message)
|
||||
=> Logged(logger, LogLevel.Error, message);
|
||||
|
||||
private static void LoggedWarning(ICacheLogger logger, string message)
|
||||
private static void LoggedWarning(ICacheLogger<PayPalController> logger, string message)
|
||||
=> Logged(logger, LogLevel.Warning, message);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.7.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0868" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
|
||||
224
test/Core.Test/Auth/Entities/AuthRequestTests.cs
Normal file
224
test/Core.Test/Auth/Entities/AuthRequestTests.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Entities;
|
||||
|
||||
public class AuthRequestTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsValidForAuthentication_WithValidRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var accessCode = "test-access-code";
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Type = AuthRequestType.AuthenticateAndUnlock,
|
||||
ResponseDate = DateTime.UtcNow,
|
||||
Approved = true,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
AuthenticationDate = null,
|
||||
AccessCode = accessCode
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = authRequest.IsValidForAuthentication(userId, accessCode);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForAuthentication_WithWrongUserId_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var differentUserId = Guid.NewGuid();
|
||||
var accessCode = "test-access-code";
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Type = AuthRequestType.AuthenticateAndUnlock,
|
||||
ResponseDate = DateTime.UtcNow,
|
||||
Approved = true,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
AuthenticationDate = null,
|
||||
AccessCode = accessCode
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = authRequest.IsValidForAuthentication(differentUserId, accessCode);
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Auth request should not validate for a different user");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForAuthentication_WithWrongAccessCode_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Type = AuthRequestType.AuthenticateAndUnlock,
|
||||
ResponseDate = DateTime.UtcNow,
|
||||
Approved = true,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
AuthenticationDate = null,
|
||||
AccessCode = "correct-code"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = authRequest.IsValidForAuthentication(userId, "wrong-code");
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForAuthentication_WithoutResponseDate_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var accessCode = "test-access-code";
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Type = AuthRequestType.AuthenticateAndUnlock,
|
||||
ResponseDate = null, // Not responded to
|
||||
Approved = true,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
AuthenticationDate = null,
|
||||
AccessCode = accessCode
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = authRequest.IsValidForAuthentication(userId, accessCode);
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Unanswered auth requests should not be valid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForAuthentication_WithApprovedFalse_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var accessCode = "test-access-code";
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Type = AuthRequestType.AuthenticateAndUnlock,
|
||||
ResponseDate = DateTime.UtcNow,
|
||||
Approved = false, // Denied
|
||||
CreationDate = DateTime.UtcNow,
|
||||
AuthenticationDate = null,
|
||||
AccessCode = accessCode
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = authRequest.IsValidForAuthentication(userId, accessCode);
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Denied auth requests should not be valid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForAuthentication_WithApprovedNull_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var accessCode = "test-access-code";
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Type = AuthRequestType.AuthenticateAndUnlock,
|
||||
ResponseDate = DateTime.UtcNow,
|
||||
Approved = null, // Pending
|
||||
CreationDate = DateTime.UtcNow,
|
||||
AuthenticationDate = null,
|
||||
AccessCode = accessCode
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = authRequest.IsValidForAuthentication(userId, accessCode);
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Pending auth requests should not be valid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForAuthentication_WithExpiredRequest_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var accessCode = "test-access-code";
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Type = AuthRequestType.AuthenticateAndUnlock,
|
||||
ResponseDate = DateTime.UtcNow,
|
||||
Approved = true,
|
||||
CreationDate = DateTime.UtcNow.AddMinutes(-20), // Expired (15 min timeout)
|
||||
AuthenticationDate = null,
|
||||
AccessCode = accessCode
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = authRequest.IsValidForAuthentication(userId, accessCode);
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Expired auth requests should not be valid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForAuthentication_WithWrongType_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var accessCode = "test-access-code";
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Type = AuthRequestType.Unlock, // Wrong type
|
||||
ResponseDate = DateTime.UtcNow,
|
||||
Approved = true,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
AuthenticationDate = null,
|
||||
AccessCode = accessCode
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = authRequest.IsValidForAuthentication(userId, accessCode);
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Only AuthenticateAndUnlock type should be valid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForAuthentication_WithAlreadyUsed_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var accessCode = "test-access-code";
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Type = AuthRequestType.AuthenticateAndUnlock,
|
||||
ResponseDate = DateTime.UtcNow,
|
||||
Approved = true,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
AuthenticationDate = DateTime.UtcNow, // Already used
|
||||
AccessCode = accessCode
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = authRequest.IsValidForAuthentication(userId, accessCode);
|
||||
|
||||
// Assert
|
||||
Assert.False(result, "Auth requests should only be valid for one-time use");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using System.Globalization;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
@@ -356,9 +357,18 @@ public class InvoiceExtensionsTests
|
||||
[Fact]
|
||||
public void FormatForProvider_ComplexScenario_HandlesAllLineTypes()
|
||||
{
|
||||
// Arrange
|
||||
var lineItems = new StripeList<InvoiceLineItem>();
|
||||
lineItems.Data = new List<InvoiceLineItem>
|
||||
// Set culture to en-US to ensure consistent decimal formatting in tests
|
||||
// This ensures tests pass on all machines regardless of system locale
|
||||
var originalCulture = Thread.CurrentThread.CurrentCulture;
|
||||
var originalUICulture = Thread.CurrentThread.CurrentUICulture;
|
||||
try
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
|
||||
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
|
||||
|
||||
// Arrange
|
||||
var lineItems = new StripeList<InvoiceLineItem>();
|
||||
lineItems.Data = new List<InvoiceLineItem>
|
||||
{
|
||||
new InvoiceLineItem
|
||||
{
|
||||
@@ -372,23 +382,29 @@ public class InvoiceExtensionsTests
|
||||
new InvoiceLineItem { Description = "Custom Service", Quantity = 2, Amount = 2000 }
|
||||
};
|
||||
|
||||
var invoice = new Invoice
|
||||
var invoice = new Invoice
|
||||
{
|
||||
Lines = lineItems,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 200 }] // Additional $2.00 tax
|
||||
};
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, result.Count);
|
||||
Assert.Equal("5 × Manage service provider (at $6.00 / month)", result[0]);
|
||||
Assert.Equal("10 × Manage service provider (at $4.00 / month)", result[1]);
|
||||
Assert.Equal("1 × Tax (at $8.00 / month)", result[2]);
|
||||
Assert.Equal("Custom Service", result[3]);
|
||||
Assert.Equal("1 × Tax (at $2.00 / month)", result[4]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lines = lineItems,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 200 }] // Additional $2.00 tax
|
||||
};
|
||||
var subscription = new Subscription();
|
||||
|
||||
// Act
|
||||
var result = invoice.FormatForProvider(subscription);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, result.Count);
|
||||
Assert.Equal("5 × Manage service provider (at $6.00 / month)", result[0]);
|
||||
Assert.Equal("10 × Manage service provider (at $4.00 / month)", result[1]);
|
||||
Assert.Equal("1 × Tax (at $8.00 / month)", result[2]);
|
||||
Assert.Equal("Custom Service", result[3]);
|
||||
Assert.Equal("1 × Tax (at $2.00 / month)", result[4]);
|
||||
Thread.CurrentThread.CurrentCulture = originalCulture;
|
||||
Thread.CurrentThread.CurrentUICulture = originalUICulture;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -328,157 +328,6 @@ public class SubscriberServiceTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetPaymentMethod
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_NullSubscriber_ThrowsArgumentNullException(
|
||||
SutProvider<SubscriberService> sutProvider) =>
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_WithNegativeStripeAccountBalance_ReturnsCorrectAccountCreditAmount(Organization organization,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
// Stripe reports balance in cents as a negative number for credit
|
||||
const int stripeAccountBalance = -593; // $5.93 credit (negative cents)
|
||||
const decimal creditAmount = 5.93M; // Same value in dollars
|
||||
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Balance = stripeAccountBalance,
|
||||
Subscriptions = new StripeList<Subscription>()
|
||||
{
|
||||
Data =
|
||||
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
|
||||
},
|
||||
InvoiceSettings = new CustomerInvoiceSettings
|
||||
{
|
||||
DefaultPaymentMethod = new PaymentMethod
|
||||
{
|
||||
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
|
||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
||||
}
|
||||
}
|
||||
};
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")
|
||||
&& options.Expand.Contains("subscriptions")
|
||||
&& options.Expand.Contains("tax_ids")))
|
||||
.Returns(customer);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPaymentMethod(organization);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(creditAmount, result.AccountCredit);
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method") &&
|
||||
options.Expand.Contains("subscriptions") &&
|
||||
options.Expand.Contains("tax_ids")));
|
||||
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_WithZeroStripeAccountBalance_ReturnsCorrectAccountCreditAmount(
|
||||
Organization organization, SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const int stripeAccountBalance = 0;
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Balance = stripeAccountBalance,
|
||||
Subscriptions = new StripeList<Subscription>()
|
||||
{
|
||||
Data =
|
||||
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
|
||||
},
|
||||
InvoiceSettings = new CustomerInvoiceSettings
|
||||
{
|
||||
DefaultPaymentMethod = new PaymentMethod
|
||||
{
|
||||
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
|
||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
||||
}
|
||||
}
|
||||
};
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")
|
||||
&& options.Expand.Contains("subscriptions")
|
||||
&& options.Expand.Contains("tax_ids")))
|
||||
.Returns(customer);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPaymentMethod(organization);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0, result.AccountCredit);
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method") &&
|
||||
options.Expand.Contains("subscriptions") &&
|
||||
options.Expand.Contains("tax_ids")));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_WithPositiveStripeAccountBalance_ReturnsCorrectAccountCreditAmount(
|
||||
Organization organization, SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const int stripeAccountBalance = 593; // $5.93 charge balance
|
||||
const decimal accountBalance = -5.93M; // account balance
|
||||
var customer = new Customer
|
||||
{
|
||||
Balance = stripeAccountBalance,
|
||||
Subscriptions = new StripeList<Subscription>()
|
||||
{
|
||||
Data =
|
||||
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
|
||||
},
|
||||
InvoiceSettings = new CustomerInvoiceSettings
|
||||
{
|
||||
DefaultPaymentMethod = new PaymentMethod
|
||||
{
|
||||
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
|
||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
||||
}
|
||||
}
|
||||
};
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")
|
||||
&& options.Expand.Contains("subscriptions")
|
||||
&& options.Expand.Contains("tax_ids")))
|
||||
.Returns(customer);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPaymentMethod(organization);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(accountBalance, result.AccountCredit);
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
|
||||
organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("default_source") &&
|
||||
options.Expand.Contains("invoice_settings.default_payment_method") &&
|
||||
options.Expand.Contains("subscriptions") &&
|
||||
options.Expand.Contains("tax_ids")));
|
||||
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GetPaymentSource
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -889,65 +738,6 @@ public class SubscriberServiceTests
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GetTaxInformation
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetTaxInformation_NullSubscriber_ThrowsArgumentNullException(
|
||||
SutProvider<SubscriberService> sutProvider) =>
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetTaxInformation(null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetTaxInformation_NullAddress_ReturnsNull(
|
||||
Organization organization,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(new Customer());
|
||||
|
||||
var taxInformation = await sutProvider.Sut.GetTaxInformation(organization);
|
||||
|
||||
Assert.Null(taxInformation);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetTaxInformation_Success(
|
||||
Organization organization,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
var address = new Address
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345",
|
||||
Line1 = "123 Example St.",
|
||||
Line2 = "Unit 1",
|
||||
City = "Example Town",
|
||||
State = "NY"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(new Customer
|
||||
{
|
||||
Address = address,
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [new TaxId { Value = "tax_id" }]
|
||||
}
|
||||
});
|
||||
|
||||
var taxInformation = await sutProvider.Sut.GetTaxInformation(organization);
|
||||
|
||||
Assert.NotNull(taxInformation);
|
||||
Assert.Equal(address.Country, taxInformation.Country);
|
||||
Assert.Equal(address.PostalCode, taxInformation.PostalCode);
|
||||
Assert.Equal("tax_id", taxInformation.TaxId);
|
||||
Assert.Equal(address.Line1, taxInformation.Line1);
|
||||
Assert.Equal(address.Line2, taxInformation.Line2);
|
||||
Assert.Equal(address.City, taxInformation.City);
|
||||
Assert.Equal(address.State, taxInformation.State);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RemovePaymentMethod
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemovePaymentMethod_NullSubscriber_ThrowsArgumentNullException(
|
||||
@@ -1844,48 +1634,6 @@ public class SubscriberServiceTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region VerifyBankAccount
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task VerifyBankAccount_NoSetupIntentId_ThrowsBillingException(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider) => await ThrowsBillingExceptionAsync(() => sutProvider.Sut.VerifyBankAccount(provider, ""));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task VerifyBankAccount_MakesCorrectInvocations(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
const string descriptorCode = "SM1234";
|
||||
|
||||
var setupIntent = new SetupIntent
|
||||
{
|
||||
Id = "setup_intent_id",
|
||||
PaymentMethodId = "payment_method_id"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
stripeAdapter.SetupIntentGet(setupIntent.Id).Returns(setupIntent);
|
||||
|
||||
await sutProvider.Sut.VerifyBankAccount(provider, descriptorCode);
|
||||
|
||||
await stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id,
|
||||
Arg.Is<SetupIntentVerifyMicrodepositsOptions>(
|
||||
options => options.DescriptorCode == descriptorCode));
|
||||
|
||||
await stripeAdapter.Received(1).PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
|
||||
Arg.Is<PaymentMethodAttachOptions>(
|
||||
options => options.Customer == provider.GatewayCustomerId));
|
||||
|
||||
await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
||||
options => options.InvoiceSettings.DefaultPaymentMethod == setupIntent.PaymentMethodId));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsValidGatewayCustomerIdAsync
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Queries;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.Queries;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class KeyConnectorConfirmationDetailsQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Run_OrganizationNotFound_Throws(SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider,
|
||||
Guid userId, string orgSsoIdentifier)
|
||||
{
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Run(orgSsoIdentifier, userId));
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ReceivedWithAnyArgs(0)
|
||||
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Run_OrganizationNotKeyConnector_Throws(
|
||||
SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider,
|
||||
Guid userId, string orgSsoIdentifier, Organization org)
|
||||
{
|
||||
org.Identifier = orgSsoIdentifier;
|
||||
org.UseKeyConnector = false;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(orgSsoIdentifier).Returns(org);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Run(orgSsoIdentifier, userId));
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ReceivedWithAnyArgs(0)
|
||||
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Run_OrganizationUserNotFound_Throws(SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider,
|
||||
Guid userId, string orgSsoIdentifier
|
||||
, Organization org)
|
||||
{
|
||||
org.Identifier = orgSsoIdentifier;
|
||||
org.UseKeyConnector = true;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(orgSsoIdentifier).Returns(org);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(Task.FromResult<OrganizationUser>(null));
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Run(orgSsoIdentifier, userId));
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.GetByOrganizationAsync(org.Id, userId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Run_Success(SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider, Guid userId,
|
||||
string orgSsoIdentifier
|
||||
, Organization org, OrganizationUser orgUser)
|
||||
{
|
||||
org.Identifier = orgSsoIdentifier;
|
||||
org.UseKeyConnector = true;
|
||||
orgUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = userId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(orgSsoIdentifier).Returns(org);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(org.Id, userId)
|
||||
.Returns(orgUser);
|
||||
|
||||
var result = await sutProvider.Sut.Run(orgSsoIdentifier, userId);
|
||||
|
||||
Assert.Equal(org.Name, result.OrganizationName);
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.GetByOrganizationAsync(org.Id, userId);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -17,506 +13,6 @@ namespace Bit.Core.Test.Services;
|
||||
[SutProviderCustomize]
|
||||
public class StripePaymentServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task
|
||||
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager =
|
||||
new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually,
|
||||
AdditionalStorage = 0
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||
p.Currency == "usd" &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripePlanId &&
|
||||
x.Quantity == 1) &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||
x.Quantity == 0)))
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 4000,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],
|
||||
Total = 4800
|
||||
});
|
||||
|
||||
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
Assert.Equal(8M, actual.TaxAmount);
|
||||
Assert.Equal(48M, actual.TotalAmount);
|
||||
Assert.Equal(40M, actual.TaxableBaseAmount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager =
|
||||
new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually,
|
||||
AdditionalStorage = 1
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||
p.Currency == "usd" &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripePlanId &&
|
||||
x.Quantity == 1) &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||
x.Quantity == 1)))
|
||||
.Returns(new Invoice { TotalExcludingTax = 4000, TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 4800 });
|
||||
|
||||
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
Assert.Equal(8M, actual.TaxAmount);
|
||||
Assert.Equal(48M, actual.TotalAmount);
|
||||
Assert.Equal(40M, actual.TaxableBaseAmount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task
|
||||
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually,
|
||||
SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise,
|
||||
AdditionalStorage = 0
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||
p.Currency == "usd" &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == "2021-family-for-enterprise-annually" &&
|
||||
x.Quantity == 1) &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||
x.Quantity == 0)))
|
||||
.Returns(new Invoice { TotalExcludingTax = 0, TotalTaxes = [new InvoiceTotalTax { Amount = 0 }], Total = 0 });
|
||||
|
||||
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
Assert.Equal(0M, actual.TaxAmount);
|
||||
Assert.Equal(0M, actual.TotalAmount);
|
||||
Assert.Equal(0M, actual.TaxableBaseAmount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task
|
||||
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually,
|
||||
SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise,
|
||||
AdditionalStorage = 1
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||
p.Currency == "usd" &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == "2021-family-for-enterprise-annually" &&
|
||||
x.Quantity == 1) &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||
x.Quantity == 1)))
|
||||
.Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 });
|
||||
|
||||
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
Assert.Equal(0.08M, actual.TaxAmount);
|
||||
Assert.Equal(4.08M, actual.TotalAmount);
|
||||
Assert.Equal(4M, actual.TaxableBaseAmount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_PersonalUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_BusinessUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsTaxExemptReverse(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithCustomerDiscount_ReturnsDiscountFromCustomer(
|
||||
|
||||
@@ -380,19 +380,16 @@ public class BaseRequestValidatorTests
|
||||
// 1 -> initial validation passes
|
||||
_sut.isValid = true;
|
||||
|
||||
// 2 -> enable the FailedTwoFactorEmail feature flag
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail).Returns(true);
|
||||
|
||||
// 3 -> set up 2FA as required
|
||||
// 2 -> set up 2FA as required
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
||||
|
||||
// 4 -> provide invalid 2FA token
|
||||
// 3 -> provide invalid 2FA token
|
||||
tokenRequest.Raw["TwoFactorToken"] = "invalid_token";
|
||||
tokenRequest.Raw["TwoFactorProvider"] = TwoFactorProviderType.Email.ToString();
|
||||
|
||||
// 5 -> set up 2FA verification to fail
|
||||
// 4 -> set up 2FA verification to fail
|
||||
_twoFactorAuthenticationValidator
|
||||
.VerifyTwoFactorAsync(user, null, TwoFactorProviderType.Email, "invalid_token")
|
||||
.Returns(Task.FromResult(false));
|
||||
@@ -427,24 +424,21 @@ public class BaseRequestValidatorTests
|
||||
// 1 -> initial validation passes
|
||||
_sut.isValid = true;
|
||||
|
||||
// 2 -> enable the FailedTwoFactorEmail feature flag
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail).Returns(true);
|
||||
|
||||
// 3 -> set up 2FA as required
|
||||
// 2 -> set up 2FA as required
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
||||
|
||||
// 4 -> provide invalid remember token (remember token expired)
|
||||
// 3 -> provide invalid remember token (remember token expired)
|
||||
tokenRequest.Raw["TwoFactorToken"] = "expired_remember_token";
|
||||
tokenRequest.Raw["TwoFactorProvider"] = "5"; // Remember provider
|
||||
|
||||
// 5 -> set up remember token verification to fail
|
||||
// 4 -> set up remember token verification to fail
|
||||
_twoFactorAuthenticationValidator
|
||||
.VerifyTwoFactorAsync(user, null, TwoFactorProviderType.Remember, "expired_remember_token")
|
||||
.Returns(Task.FromResult(false));
|
||||
|
||||
// 6 -> set up dummy BuildTwoFactorResultAsync
|
||||
// 5 -> set up dummy BuildTwoFactorResultAsync
|
||||
var twoFactorResultDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "TwoFactorProviders", new[] { "0", "1" } },
|
||||
@@ -1127,9 +1121,6 @@ public class BaseRequestValidatorTests
|
||||
.VerifyTwoFactorAsync(user, null, TwoFactorProviderType.RecoveryCode, "INVALID-recovery-code")
|
||||
.Returns(Task.FromResult(false));
|
||||
|
||||
// 6. Setup for failed 2FA email (if feature flag enabled)
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail).Returns(true);
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||
@@ -313,4 +315,66 @@ public class UserRepositoryTests
|
||||
Assert.Equal(sqlUser.MasterPasswordHint, updatedUser.MasterPasswordHint);
|
||||
Assert.Equal(sqlUser.Email, updatedUser.Email);
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfUserAutoData]
|
||||
public async Task UpdateAccountCryptographicStateAsync_Works_DataMatches(
|
||||
User user,
|
||||
List<EfRepo.UserRepository> suts,
|
||||
SqlRepo.UserRepository sqlUserRepo)
|
||||
{
|
||||
// Test for V1 user (no signature key pair or security state)
|
||||
var accountKeysDataV1 = new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
wrappedPrivateKey: "v1-wrapped-private-key",
|
||||
publicKey: "v1-public-key"
|
||||
)
|
||||
};
|
||||
|
||||
foreach (var sut in suts)
|
||||
{
|
||||
var createdUser = await sut.CreateAsync(user);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
await sut.SetV2AccountCryptographicStateAsync(createdUser.Id, accountKeysDataV1);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
var updatedUser = await sut.GetByIdAsync(createdUser.Id);
|
||||
Assert.Equal("v1-public-key", updatedUser.PublicKey);
|
||||
Assert.Equal("v1-wrapped-private-key", updatedUser.PrivateKey);
|
||||
Assert.Null(updatedUser.SignedPublicKey);
|
||||
Assert.Null(updatedUser.SecurityState);
|
||||
Assert.Null(updatedUser.SecurityVersion);
|
||||
}
|
||||
|
||||
// Test for V2 user (with signature key pair and security state)
|
||||
var accountKeysDataV2 = new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
wrappedPrivateKey: "v2-wrapped-private-key",
|
||||
publicKey: "v2-public-key",
|
||||
signedPublicKey: "v2-signed-public-key"
|
||||
),
|
||||
SignatureKeyPairData = new SignatureKeyPairData(
|
||||
signatureAlgorithm: SignatureAlgorithm.Ed25519,
|
||||
wrappedSigningKey: "v2-wrapped-signing-key",
|
||||
verifyingKey: "v2-verifying-key"
|
||||
),
|
||||
SecurityStateData = new SecurityStateData
|
||||
{
|
||||
SecurityState = "v2-security-state",
|
||||
SecurityVersion = 2
|
||||
}
|
||||
};
|
||||
|
||||
var sqlUser = await sqlUserRepo.CreateAsync(user);
|
||||
await sqlUserRepo.SetV2AccountCryptographicStateAsync(sqlUser.Id, accountKeysDataV2);
|
||||
|
||||
var updatedSqlUser = await sqlUserRepo.GetByIdAsync(sqlUser.Id);
|
||||
Assert.Equal("v2-public-key", updatedSqlUser.PublicKey);
|
||||
Assert.Equal("v2-wrapped-private-key", updatedSqlUser.PrivateKey);
|
||||
Assert.Equal("v2-signed-public-key", updatedSqlUser.SignedPublicKey);
|
||||
Assert.Equal("v2-security-state", updatedSqlUser.SecurityState);
|
||||
Assert.Equal(2, updatedSqlUser.SecurityVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,4 +225,58 @@ public class CipherRepositoryTests
|
||||
Assert.True(savedCipher == null);
|
||||
}
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
|
||||
public async Task ReplaceAsync_WithCollections_UpdatesFoldersFavoritesRepromptAndArchivedDateAsync(
|
||||
Cipher cipher,
|
||||
User user,
|
||||
Organization org,
|
||||
Collection collection,
|
||||
List<EfVaultRepo.CipherRepository> suts,
|
||||
List<EfRepo.UserRepository> efUserRepos,
|
||||
List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||
List<EfRepo.CollectionRepository> efCollectionRepos)
|
||||
{
|
||||
foreach (var sut in suts)
|
||||
{
|
||||
var i = suts.IndexOf(sut);
|
||||
|
||||
var postEfOrg = await efOrgRepos[i].CreateAsync(org);
|
||||
efOrgRepos[i].ClearChangeTracking();
|
||||
var postEfUser = await efUserRepos[i].CreateAsync(user);
|
||||
efUserRepos[i].ClearChangeTracking();
|
||||
|
||||
collection.OrganizationId = postEfOrg.Id;
|
||||
var postEfCollection = await efCollectionRepos[i].CreateAsync(collection);
|
||||
efCollectionRepos[i].ClearChangeTracking();
|
||||
|
||||
cipher.UserId = postEfUser.Id;
|
||||
cipher.OrganizationId = null;
|
||||
cipher.Folders = $"{{\"{postEfUser.Id}\":\"some-folder-id\"}}";
|
||||
cipher.Favorites = $"{{\"{postEfUser.Id}\":true}}";
|
||||
cipher.Reprompt = Core.Vault.Enums.CipherRepromptType.Password;
|
||||
|
||||
var createdCipher = await sut.CreateAsync(cipher);
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
var updatedCipher = await sut.GetByIdAsync(createdCipher.Id);
|
||||
updatedCipher.UserId = postEfUser.Id;
|
||||
updatedCipher.OrganizationId = postEfOrg.Id;
|
||||
updatedCipher.Folders = $"{{\"{postEfUser.Id}\":\"new-folder-id\"}}";
|
||||
updatedCipher.Favorites = $"{{\"{postEfUser.Id}\":true}}";
|
||||
updatedCipher.Reprompt = Core.Vault.Enums.CipherRepromptType.Password;
|
||||
|
||||
await sut.ReplaceAsync(updatedCipher, new List<Guid> { postEfCollection.Id });
|
||||
sut.ClearChangeTracking();
|
||||
|
||||
|
||||
var savedCipher = await sut.GetByIdAsync(createdCipher.Id);
|
||||
Assert.NotNull(savedCipher);
|
||||
Assert.Null(savedCipher.UserId);
|
||||
Assert.Equal(postEfOrg.Id, savedCipher.OrganizationId);
|
||||
Assert.Equal($"{{\"{postEfUser.Id}\":\"new-folder-id\"}}", savedCipher.Folders);
|
||||
Assert.Equal($"{{\"{postEfUser.Id}\":true}}", savedCipher.Favorites);
|
||||
Assert.Equal(Core.Vault.Enums.CipherRepromptType.Password, savedCipher.Reprompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user