mirror of
https://github.com/bitwarden/server
synced 2026-01-14 14:33:51 +00:00
Merge branch 'main' of github.com:bitwarden/server into arch/seeder-api
This commit is contained in:
@@ -5,7 +5,6 @@ using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
@@ -14,8 +13,6 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
|
||||
@@ -28,12 +25,6 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
public OrganizationUserControllerTests(ApiApplicationFactory apiFactory)
|
||||
{
|
||||
_factory = apiFactory;
|
||||
_factory.SubstituteService<IFeatureService>(featureService =>
|
||||
{
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
});
|
||||
_client = _factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
using System.Net;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
|
||||
|
||||
public class OrganizationUsersControllerSelfRevokeTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
private readonly LoginHelper _loginHelper;
|
||||
|
||||
private string _ownerEmail = null!;
|
||||
|
||||
public OrganizationUsersControllerSelfRevokeTests(ApiApplicationFactory apiFactory)
|
||||
{
|
||||
_factory = apiFactory;
|
||||
_client = _factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ownerEmail = $"{Guid.NewGuid()}@example.com";
|
||||
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SelfRevoke_WhenPolicyEnabledAndUserIsEligible_ReturnsOk()
|
||||
{
|
||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
|
||||
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
var policy = new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true,
|
||||
Data = null
|
||||
};
|
||||
await _factory.GetService<IPolicyRepository>().CreateAsync(policy);
|
||||
|
||||
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
|
||||
_factory,
|
||||
organization.Id,
|
||||
OrganizationUserType.User);
|
||||
await _loginHelper.LoginAsync(userEmail);
|
||||
|
||||
var result = await _client.PutAsync($"organizations/{organization.Id}/users/revoke-self", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, result.StatusCode);
|
||||
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(organization.Id);
|
||||
var revokedUser = organizationUsers.FirstOrDefault(u => u.Email == userEmail);
|
||||
|
||||
Assert.NotNull(revokedUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Revoked, revokedUser.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SelfRevoke_WhenUserNotMemberOfOrganization_ReturnsForbidden()
|
||||
{
|
||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
|
||||
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
var policy = new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true,
|
||||
Data = null
|
||||
};
|
||||
await _factory.GetService<IPolicyRepository>().CreateAsync(policy);
|
||||
|
||||
var nonMemberEmail = $"{Guid.NewGuid()}@example.com";
|
||||
await _factory.LoginWithNewAccount(nonMemberEmail);
|
||||
await _loginHelper.LoginAsync(nonMemberEmail);
|
||||
|
||||
var result = await _client.PutAsync($"organizations/{organization.Id}/users/revoke-self", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrganizationUserType.Owner)]
|
||||
[InlineData(OrganizationUserType.Admin)]
|
||||
public async Task SelfRevoke_WhenUserIsOwnerOrAdmin_ReturnsBadRequest(OrganizationUserType userType)
|
||||
{
|
||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
|
||||
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
var policy = new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true,
|
||||
Data = null
|
||||
};
|
||||
await _factory.GetService<IPolicyRepository>().CreateAsync(policy);
|
||||
|
||||
string userEmail;
|
||||
if (userType == OrganizationUserType.Owner)
|
||||
{
|
||||
userEmail = _ownerEmail;
|
||||
}
|
||||
else
|
||||
{
|
||||
(userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
|
||||
_factory,
|
||||
organization.Id,
|
||||
userType);
|
||||
}
|
||||
|
||||
await _loginHelper.LoginAsync(userEmail);
|
||||
|
||||
var result = await _client.PutAsync($"organizations/{organization.Id}/users/revoke-self", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SelfRevoke_WhenUserIsProviderButNotMember_ReturnsForbidden()
|
||||
{
|
||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
|
||||
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
var policy = new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true,
|
||||
Data = null
|
||||
};
|
||||
await _factory.GetService<IPolicyRepository>().CreateAsync(policy);
|
||||
|
||||
var provider = await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(
|
||||
_factory,
|
||||
organization.Id,
|
||||
ProviderType.Msp,
|
||||
ProviderStatusType.Billable);
|
||||
|
||||
var providerUserEmail = $"{Guid.NewGuid()}@example.com";
|
||||
await _factory.LoginWithNewAccount(providerUserEmail);
|
||||
await ProviderTestHelpers.CreateProviderUserAsync(
|
||||
_factory,
|
||||
provider.Id,
|
||||
providerUserEmail,
|
||||
ProviderUserType.ProviderAdmin);
|
||||
|
||||
await _loginHelper.LoginAsync(providerUserEmail);
|
||||
|
||||
var result = await _client.PutAsync($"organizations/{organization.Id}/users/revoke-self", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Api.Models.Public.Request;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.Controllers.Public;
|
||||
|
||||
public class CollectionsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
private readonly LoginHelper _loginHelper;
|
||||
|
||||
private string _ownerEmail = null!;
|
||||
private Organization _organization = null!;
|
||||
|
||||
public CollectionsControllerTests(ApiApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_factory.SubstituteService<IPushNotificationService>(_ => { });
|
||||
_factory.SubstituteService<IFeatureService>(_ => { });
|
||||
_client = factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||
|
||||
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||
plan: PlanType.EnterpriseAnnually,
|
||||
ownerEmail: _ownerEmail,
|
||||
passwordManagerSeats: 10,
|
||||
paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCollectionWithMultipleUsersAndVariedPermissions_Success()
|
||||
{
|
||||
// Arrange
|
||||
_organization.AllowAdminAccessToAllCollectionItems = true;
|
||||
await _factory.GetService<IOrganizationRepository>().UpsertAsync(_organization);
|
||||
|
||||
var groupRepository = _factory.GetService<IGroupRepository>();
|
||||
var group = await groupRepository.CreateAsync(new Group
|
||||
{
|
||||
OrganizationId = _organization.Id,
|
||||
Name = "CollectionControllerTests.CreateCollectionWithMultipleUsersAndVariedPermissions_Success",
|
||||
ExternalId = $"CollectionControllerTests.CreateCollectionWithMultipleUsersAndVariedPermissions_Success{Guid.NewGuid()}",
|
||||
});
|
||||
|
||||
var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
|
||||
_factory,
|
||||
_organization.Id,
|
||||
OrganizationUserType.User);
|
||||
|
||||
var collection = await OrganizationTestHelpers.CreateCollectionAsync(
|
||||
_factory,
|
||||
_organization.Id,
|
||||
"Shared Collection with a group",
|
||||
externalId: "shared-collection-with-group",
|
||||
groups:
|
||||
[
|
||||
new CollectionAccessSelection { Id = group.Id, ReadOnly = false, HidePasswords = false, Manage = true }
|
||||
],
|
||||
users:
|
||||
[
|
||||
new CollectionAccessSelection { Id = user.Id, ReadOnly = false, HidePasswords = false, Manage = true }
|
||||
]);
|
||||
|
||||
var getCollectionsResponse = await _client.GetFromJsonAsync<ListResponseModel<CollectionResponseModel>>("public/collections");
|
||||
var getCollectionResponse = await _client.GetFromJsonAsync<CollectionResponseModel>($"public/collections/{collection.Id}");
|
||||
|
||||
var firstCollection = getCollectionsResponse.Data.First(x => x.ExternalId == "shared-collection-with-group");
|
||||
|
||||
var update = new CollectionUpdateRequestModel
|
||||
{
|
||||
ExternalId = firstCollection.ExternalId,
|
||||
Groups = firstCollection.Groups?.Select(x => new AssociationWithPermissionsRequestModel
|
||||
{
|
||||
Id = x.Id,
|
||||
ReadOnly = x.ReadOnly,
|
||||
HidePasswords = x.HidePasswords,
|
||||
Manage = x.Manage
|
||||
}),
|
||||
};
|
||||
|
||||
await _client.PutAsJsonAsync($"public/collections/{firstCollection.Id}", update);
|
||||
|
||||
var result = await _factory.GetService<ICollectionRepository>()
|
||||
.GetByIdWithAccessAsync(firstCollection.Id);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotEmpty(result.Item2.Groups);
|
||||
Assert.NotEmpty(result.Item2.Users);
|
||||
}
|
||||
}
|
||||
@@ -159,14 +159,16 @@ public static class OrganizationTestHelpers
|
||||
Guid organizationId,
|
||||
string name,
|
||||
IEnumerable<CollectionAccessSelection>? users = null,
|
||||
IEnumerable<CollectionAccessSelection>? groups = null)
|
||||
IEnumerable<CollectionAccessSelection>? groups = null,
|
||||
string? externalId = null)
|
||||
{
|
||||
var collectionRepository = factory.GetService<ICollectionRepository>();
|
||||
var collection = new Collection
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Name = name,
|
||||
Type = CollectionType.SharedCollection
|
||||
Type = CollectionType.SharedCollection,
|
||||
ExternalId = externalId
|
||||
};
|
||||
|
||||
await collectionRepository.CreateAsync(collection, groups, users);
|
||||
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@@ -19,9 +20,11 @@ using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Vault.Enums;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.KeyManagement.Controllers;
|
||||
@@ -31,6 +34,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
private static readonly string _mockEncryptedString =
|
||||
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||
private static readonly string _mockEncryptedType7String = "7.AOs41Hd8OQiCPXjyJKCiDA==";
|
||||
private static readonly string _mockEncryptedType7WrappedSigningKey = "7.DRv74Kg1RSlFSam1MNFlGD==";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||
@@ -47,8 +51,11 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_factory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
|
||||
"true");
|
||||
_factory.SubstituteService<IFeatureService>(featureService =>
|
||||
{
|
||||
featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration, Arg.Any<bool>())
|
||||
.Returns(true);
|
||||
});
|
||||
_client = factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
_userRepository = _factory.GetService<IUserRepository>();
|
||||
@@ -78,8 +85,11 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
{
|
||||
// Localize factory to inject a false value for the feature flag.
|
||||
var localFactory = new ApiApplicationFactory();
|
||||
localFactory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
|
||||
"false");
|
||||
localFactory.SubstituteService<IFeatureService>(featureService =>
|
||||
{
|
||||
featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration, Arg.Any<bool>())
|
||||
.Returns(false);
|
||||
});
|
||||
var localClient = localFactory.CreateClient();
|
||||
var localEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
var localLoginHelper = new LoginHelper(localFactory, localClient);
|
||||
@@ -285,21 +295,21 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier,
|
||||
SetKeyConnectorKeyRequestModel request)
|
||||
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier)
|
||||
{
|
||||
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);
|
||||
|
||||
var ssoUser = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||
Assert.NotNull(ssoUser);
|
||||
|
||||
request.Keys = new KeysRequestModel
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
PublicKey = ssoUser.PublicKey,
|
||||
EncryptedPrivateKey = ssoUser.PrivateKey
|
||||
Key = _mockEncryptedString,
|
||||
Keys = new KeysRequestModel { PublicKey = ssoUser.PublicKey, EncryptedPrivateKey = ssoUser.PrivateKey },
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
OrgIdentifier = organizationSsoIdentifier
|
||||
};
|
||||
request.Key = _mockEncryptedString;
|
||||
request.OrgIdentifier = organizationSsoIdentifier;
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
@@ -310,12 +320,95 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
Assert.True(user.UsesKeyConnector);
|
||||
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
|
||||
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||
var ssoOrganizationUser = await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
|
||||
Assert.NotNull(ssoOrganizationUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
|
||||
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
|
||||
Assert.Null(ssoOrganizationUser.Email);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostSetKeyConnectorKeyAsync_V2_NotLoggedIn_Unauthorized()
|
||||
{
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = _mockEncryptedString,
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = "publicKey",
|
||||
UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String
|
||||
},
|
||||
OrgIdentifier = "test-org"
|
||||
};
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_V2_Success(string organizationSsoIdentifier)
|
||||
{
|
||||
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);
|
||||
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = _mockEncryptedString,
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = "publicKey",
|
||||
UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String,
|
||||
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
|
||||
{
|
||||
PublicKey = "publicKey",
|
||||
WrappedPrivateKey = _mockEncryptedType7String,
|
||||
SignedPublicKey = "signedPublicKey"
|
||||
},
|
||||
SignatureKeyPair = new SignatureKeyPairRequestModel
|
||||
{
|
||||
SignatureAlgorithm = "ed25519",
|
||||
WrappedSigningKey = _mockEncryptedType7WrappedSigningKey,
|
||||
VerifyingKey = "verifyingKey"
|
||||
},
|
||||
SecurityState = new SecurityStateModel
|
||||
{
|
||||
SecurityVersion = 2,
|
||||
SecurityState = "v2"
|
||||
}
|
||||
},
|
||||
OrgIdentifier = organizationSsoIdentifier
|
||||
};
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
|
||||
Assert.NotNull(user);
|
||||
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, user.Key);
|
||||
Assert.True(user.UsesKeyConnector);
|
||||
Assert.Equal(KdfType.Argon2id, user.Kdf);
|
||||
Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, user.KdfIterations);
|
||||
Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, user.KdfMemory);
|
||||
Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, user.KdfParallelism);
|
||||
Assert.Equal(request.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey, user.SignedPublicKey);
|
||||
Assert.Equal(request.AccountKeys.SecurityState!.SecurityState, user.SecurityState);
|
||||
Assert.Equal(request.AccountKeys.SecurityState.SecurityVersion, user.SecurityVersion);
|
||||
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
|
||||
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||
|
||||
var ssoOrganizationUser =
|
||||
await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
|
||||
Assert.NotNull(ssoOrganizationUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
|
||||
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
|
||||
Assert.Null(ssoOrganizationUser.Email);
|
||||
|
||||
var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id);
|
||||
Assert.NotNull(signatureKeyPair);
|
||||
Assert.Equal(SignatureAlgorithm.Ed25519, signatureKeyPair.SignatureAlgorithm);
|
||||
Assert.Equal(_mockEncryptedType7WrappedSigningKey, signatureKeyPair.WrappedSigningKey);
|
||||
Assert.Equal("verifyingKey", signatureKeyPair.VerifyingKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using Bit.Api.AdminConsole.Authorization.Requirements;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Authorization.Requirements;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class MemberRequirementTests
|
||||
{
|
||||
[Theory]
|
||||
[CurrentContextOrganizationCustomize]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
public async Task AuthorizeAsync_WhenUserIsOrganizationMember_ThenRequestShouldBeAuthorized(
|
||||
OrganizationUserType type,
|
||||
CurrentContextOrganization organization,
|
||||
SutProvider<MemberRequirement> sutProvider)
|
||||
{
|
||||
organization.Type = type;
|
||||
|
||||
var actual = await sutProvider.Sut.AuthorizeAsync(organization, () => Task.FromResult(false));
|
||||
|
||||
Assert.True(actual);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AuthorizeAsync_WhenUserIsNotOrganizationMember_ThenRequestShouldBeDenied(
|
||||
SutProvider<MemberRequirement> sutProvider)
|
||||
{
|
||||
var actual = await sutProvider.Sut.AuthorizeAsync(null, () => Task.FromResult(false));
|
||||
|
||||
Assert.False(actual);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AuthorizeAsync_WhenUserIsProviderButNotMember_ThenRequestShouldBeDenied(
|
||||
SutProvider<MemberRequirement> sutProvider)
|
||||
{
|
||||
var actual = await sutProvider.Sut.AuthorizeAsync(null, () => Task.FromResult(true));
|
||||
|
||||
Assert.False(actual);
|
||||
}
|
||||
}
|
||||
@@ -66,8 +66,8 @@ public class ProviderClientsControllerTests
|
||||
signup.Plan == requestBody.PlanType &&
|
||||
signup.AdditionalSeats == requestBody.Seats &&
|
||||
signup.OwnerKey == requestBody.Key &&
|
||||
signup.PublicKey == requestBody.KeyPair.PublicKey &&
|
||||
signup.PrivateKey == requestBody.KeyPair.EncryptedPrivateKey &&
|
||||
signup.Keys.PublicKey == requestBody.KeyPair.PublicKey &&
|
||||
signup.Keys.WrappedPrivateKey == requestBody.KeyPair.EncryptedPrivateKey &&
|
||||
signup.CollectionName == requestBody.CollectionName),
|
||||
requestBody.OwnerEmail,
|
||||
user)
|
||||
|
||||
@@ -49,6 +49,7 @@ public class ProfileOrganizationResponseModelTests
|
||||
UseCustomPermissions = organization.UseCustomPermissions,
|
||||
UseRiskInsights = organization.UseRiskInsights,
|
||||
UsePhishingBlocker = organization.UsePhishingBlocker,
|
||||
UseDisableSMAdsForUsers = organization.UseDisableSmAdsForUsers,
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains,
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies,
|
||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation,
|
||||
|
||||
@@ -46,6 +46,7 @@ public class ProfileProviderOrganizationResponseModelTests
|
||||
UseCustomPermissions = organization.UseCustomPermissions,
|
||||
UseRiskInsights = organization.UseRiskInsights,
|
||||
UsePhishingBlocker = organization.UsePhishingBlocker,
|
||||
UseDisableSMAdsForUsers = organization.UseDisableSmAdsForUsers,
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains,
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies,
|
||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.Billing.Controllers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -29,8 +27,6 @@ public class AccountsControllerTests : IDisposable
|
||||
private readonly IUserService _userService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IStripePaymentService _paymentService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly AccountsController _sut;
|
||||
@@ -40,15 +36,11 @@ public class AccountsControllerTests : IDisposable
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_paymentService = Substitute.For<IStripePaymentService>();
|
||||
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
||||
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
||||
_licensingService = Substitute.For<ILicensingService>();
|
||||
_globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
_sut = new AccountsController(
|
||||
_userService,
|
||||
_twoFactorIsEnabledQuery,
|
||||
_userAccountKeysQuery,
|
||||
_featureService,
|
||||
_licensingService
|
||||
);
|
||||
@@ -85,6 +77,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
@@ -124,6 +117,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(false);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
@@ -161,6 +155,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
@@ -207,6 +202,7 @@ public class AccountsControllerTests : IDisposable
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_userService.GenerateLicenseAsync(user).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
@@ -243,6 +239,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
@@ -293,6 +290,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
@@ -349,6 +347,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act & Assert - Feature flag ENABLED
|
||||
@@ -413,6 +412,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act - Step 4: Call AccountsController.GetSubscriptionAsync
|
||||
@@ -507,6 +507,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
@@ -558,6 +559,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
@@ -611,6 +613,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
@@ -658,6 +661,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
@@ -726,6 +730,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act - Full pipeline: Stripe → SubscriptionInfo → SubscriptionResponseModel → API response
|
||||
@@ -791,6 +796,7 @@ public class AccountsControllerTests : IDisposable
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_userService.GenerateLicenseAsync(user).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); // Flag enabled
|
||||
|
||||
// Act
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
using Bit.Api.Billing.Controllers.VNext;
|
||||
using Bit.Api.Billing.Models.Requests.Storage;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Licenses.Queries;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NSubstitute;
|
||||
using OneOf.Types;
|
||||
using Xunit;
|
||||
using BadRequest = Bit.Core.Billing.Commands.BadRequest;
|
||||
|
||||
namespace Bit.Api.Test.Billing.Controllers.VNext;
|
||||
|
||||
public class AccountBillingVNextControllerTests
|
||||
{
|
||||
private readonly IUpdatePremiumStorageCommand _updatePremiumStorageCommand;
|
||||
private readonly IGetUserLicenseQuery _getUserLicenseQuery;
|
||||
private readonly AccountBillingVNextController _sut;
|
||||
|
||||
public AccountBillingVNextControllerTests()
|
||||
{
|
||||
_updatePremiumStorageCommand = Substitute.For<IUpdatePremiumStorageCommand>();
|
||||
_getUserLicenseQuery = Substitute.For<IGetUserLicenseQuery>();
|
||||
|
||||
_sut = new AccountBillingVNextController(
|
||||
Substitute.For<Core.Billing.Payment.Commands.ICreateBitPayInvoiceForCreditCommand>(),
|
||||
Substitute.For<Core.Billing.Premium.Commands.ICreatePremiumCloudHostedSubscriptionCommand>(),
|
||||
Substitute.For<Core.Billing.Payment.Queries.IGetCreditQuery>(),
|
||||
Substitute.For<Core.Billing.Payment.Queries.IGetPaymentMethodQuery>(),
|
||||
_getUserLicenseQuery,
|
||||
Substitute.For<Core.Billing.Payment.Commands.IUpdatePaymentMethodCommand>(),
|
||||
_updatePremiumStorageCommand);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetLicenseAsync_ValidUser_ReturnsLicenseResponse(
|
||||
User user,
|
||||
Core.Billing.Licenses.Models.Api.Response.LicenseResponseModel licenseResponse)
|
||||
{
|
||||
// Arrange
|
||||
_getUserLicenseQuery.Run(user).Returns(licenseResponse);
|
||||
// Act
|
||||
var result = await _sut.GetLicenseAsync(user);
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _getUserLicenseQuery.Received(1).Run(user);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_Success_ReturnsOk(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 10 };
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 10))
|
||||
.Returns(new BillingCommandResult<None>(new None()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 10);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_UserNotPremium_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 10 };
|
||||
var errorMessage = "User does not have a premium subscription.";
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 10))
|
||||
.Returns(new BadRequest(errorMessage));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 10);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_NoPaymentMethod_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 10 };
|
||||
var errorMessage = "No payment method found.";
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 10))
|
||||
.Returns(new BadRequest(errorMessage));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 10);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_StorageLessThanBase_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 1 };
|
||||
var errorMessage = "Storage cannot be less than the base amount of 1 GB.";
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 1))
|
||||
.Returns(new BadRequest(errorMessage));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 1);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_StorageExceedsMaximum_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 100 };
|
||||
var errorMessage = "Maximum storage is 100 GB.";
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 100))
|
||||
.Returns(new BadRequest(errorMessage));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 100);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_StorageExceedsCurrentUsage_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 2 };
|
||||
var errorMessage = "You are currently using 5.00 GB of storage. Delete some stored data first.";
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 2))
|
||||
.Returns(new BadRequest(errorMessage));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 2);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_IncreaseStorage_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 15 };
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 15))
|
||||
.Returns(new BillingCommandResult<None>(new None()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 15);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_DecreaseStorage_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 3 };
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 3))
|
||||
.Returns(new BillingCommandResult<None>(new None()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 3);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_MaximumStorage_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 100 };
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 100))
|
||||
.Returns(new BillingCommandResult<None>(new None()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 100);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateStorageAsync_NullPaymentSecret_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
var request = new StorageUpdateRequest { AdditionalStorageGb = 5 };
|
||||
|
||||
_updatePremiumStorageCommand.Run(
|
||||
Arg.Is<User>(u => u.Id == user.Id),
|
||||
Arg.Is<short>(s => s == 5))
|
||||
.Returns(new BillingCommandResult<None>(new None()));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateStorageAsync(user, request);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _updatePremiumStorageCommand.Received(1).Run(user, 5);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
using Bit.Api.Dirt.Controllers;
|
||||
using Bit.Api.Dirt.Models.Request;
|
||||
using Bit.Api.Dirt.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
namespace Bit.Api.Test.Dirt.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(OrganizationIntegrationController))]
|
||||
[SutProviderCustomize]
|
||||
@@ -1,9 +1,9 @@
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
|
||||
using Bit.Api.Dirt.Controllers;
|
||||
using Bit.Api.Dirt.Models.Request;
|
||||
using Bit.Api.Dirt.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
namespace Bit.Api.Test.Dirt.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(OrganizationIntegrationConfigurationController))]
|
||||
[SutProviderCustomize]
|
||||
@@ -1,13 +1,13 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Api.Dirt.Controllers;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -16,7 +16,7 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
namespace Bit.Api.Test.Dirt.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(SlackIntegrationController))]
|
||||
[SutProviderCustomize]
|
||||
@@ -1,14 +1,14 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Api.Dirt.Controllers;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Models.Data.Teams;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Teams;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -20,7 +20,7 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
namespace Bit.Api.Test.Dirt.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(TeamsIntegrationController))]
|
||||
[SutProviderCustomize]
|
||||
@@ -1,13 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Api.Dirt.Models.Request;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;
|
||||
namespace Bit.Api.Test.Dirt.Models.Request;
|
||||
|
||||
public class OrganizationIntegrationRequestModelTests
|
||||
{
|
||||
@@ -1,15 +1,15 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Teams;
|
||||
using Bit.Api.Dirt.Models.Response;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Models.Data.Teams;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Response.Organizations;
|
||||
namespace Bit.Api.Test.Dirt.Models.Response;
|
||||
|
||||
public class OrganizationIntegrationResponseModelTests
|
||||
{
|
||||
@@ -238,10 +238,13 @@ public class AccountsKeyManagementControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_UserNull_Throws(
|
||||
public async Task PostSetKeyConnectorKeyAsync_V1_UserNull_Throws(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
SetKeyConnectorKeyRequestModel data)
|
||||
{
|
||||
data.KeyConnectorKeyWrappedUserKey = null;
|
||||
data.AccountKeys = null;
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
|
||||
@@ -252,10 +255,13 @@ public class AccountsKeyManagementControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
|
||||
public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
SetKeyConnectorKeyRequestModel data, User expectedUser)
|
||||
{
|
||||
data.KeyConnectorKeyWrappedUserKey = null;
|
||||
data.AccountKeys = null;
|
||||
|
||||
expectedUser.PublicKey = null;
|
||||
expectedUser.PrivateKey = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
@@ -278,17 +284,20 @@ public class AccountsKeyManagementControllerTests
|
||||
Assert.Equal(data.KdfIterations, user.KdfIterations);
|
||||
Assert.Equal(data.KdfMemory, user.KdfMemory);
|
||||
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
|
||||
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
|
||||
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
|
||||
Assert.Equal(data.Keys!.PublicKey, user.PublicKey);
|
||||
Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey);
|
||||
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeySucceeds_OkResponse(
|
||||
public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeySucceeds_OkResponse(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
SetKeyConnectorKeyRequestModel data, User expectedUser)
|
||||
{
|
||||
data.KeyConnectorKeyWrappedUserKey = null;
|
||||
data.AccountKeys = null;
|
||||
|
||||
expectedUser.PublicKey = null;
|
||||
expectedUser.PrivateKey = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
@@ -308,11 +317,108 @@ public class AccountsKeyManagementControllerTests
|
||||
Assert.Equal(data.KdfIterations, user.KdfIterations);
|
||||
Assert.Equal(data.KdfMemory, user.KdfMemory);
|
||||
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
|
||||
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
|
||||
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
|
||||
Assert.Equal(data.Keys!.PublicKey, user.PublicKey);
|
||||
Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey);
|
||||
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_V2_UserNull_Throws(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider)
|
||||
{
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = "public-key",
|
||||
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
|
||||
},
|
||||
OrgIdentifier = "test-org"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request));
|
||||
|
||||
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().DidNotReceive()
|
||||
.SetKeyConnectorKeyForUserAsync(Arg.Any<User>(), Arg.Any<KeyConnectorKeysData>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_V2_Success(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
User expectedUser)
|
||||
{
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = "public-key",
|
||||
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
|
||||
},
|
||||
OrgIdentifier = "test-org"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(expectedUser);
|
||||
|
||||
await sutProvider.Sut.PostSetKeyConnectorKeyAsync(request);
|
||||
|
||||
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().Received(1)
|
||||
.SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser),
|
||||
Arg.Do<KeyConnectorKeysData>(data =>
|
||||
{
|
||||
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
|
||||
Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey);
|
||||
Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey,
|
||||
data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
|
||||
Assert.Equal(request.OrgIdentifier, data.OrgIdentifier);
|
||||
}));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostSetKeyConnectorKeyAsync_V2_CommandThrows_PropagatesException(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
User expectedUser)
|
||||
{
|
||||
var request = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = "public-key",
|
||||
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
|
||||
},
|
||||
OrgIdentifier = "test-org"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(expectedUser);
|
||||
sutProvider.GetDependency<ISetKeyConnectorKeyCommand>()
|
||||
.When(x => x.SetKeyConnectorKeyForUserAsync(Arg.Any<User>(), Arg.Any<KeyConnectorKeysData>()))
|
||||
.Do(_ => throw new BadRequestException("Command failed"));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request));
|
||||
|
||||
Assert.Equal("Command failed", exception.Message);
|
||||
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().Received(1)
|
||||
.SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser),
|
||||
Arg.Do<KeyConnectorKeysData>(data =>
|
||||
{
|
||||
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
|
||||
Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey);
|
||||
Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey,
|
||||
data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
|
||||
Assert.Equal(request.OrgIdentifier, data.OrgIdentifier);
|
||||
}));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostConvertToKeyConnectorAsync_UserNull_Throws(
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.KeyManagement.Models.Request;
|
||||
|
||||
public class SetKeyConnectorKeyRequestModelTests
|
||||
{
|
||||
private const string _wrappedUserKey = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||
private const string _publicKey = "public-key";
|
||||
private const string _privateKey = "private-key";
|
||||
private const string _userKey = "user-key";
|
||||
private const string _orgIdentifier = "org-identifier";
|
||||
|
||||
[Fact]
|
||||
public void Validate_V2Registration_Valid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = _publicKey,
|
||||
UserKeyEncryptedAccountPrivateKey = _privateKey
|
||||
},
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V2Registration_WrappedUserKeyNotEncryptedString_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = "not-encrypted-string",
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = _publicKey,
|
||||
UserKeyEncryptedAccountPrivateKey = _privateKey
|
||||
},
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results,
|
||||
r => r.ErrorMessage == "KeyConnectorKeyWrappedUserKey is not a valid encrypted string.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_Valid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_MissingKey_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = null,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "Key must be supplied.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_MissingKeys_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = null,
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "Keys must be supplied.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_MissingKdf_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = null,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "Kdf must be supplied.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_MissingKdfIterations_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = null,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "KdfIterations must be supplied.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_Argon2id_MissingKdfMemory_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = KdfType.Argon2id,
|
||||
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
KdfMemory = null,
|
||||
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "KdfMemory must be supplied when Kdf is Argon2id.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_V1Registration_Argon2id_MissingKdfParallelism_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
Key = _userKey,
|
||||
Keys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = _publicKey,
|
||||
EncryptedPrivateKey = _privateKey
|
||||
},
|
||||
Kdf = KdfType.Argon2id,
|
||||
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
|
||||
KdfParallelism = null,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = Validate(model);
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Contains(results, r => r.ErrorMessage == "KdfParallelism must be supplied when Kdf is Argon2id.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToKeyConnectorKeysData_EmptyKeyConnectorKeyWrappedUserKey_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = "",
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = _publicKey,
|
||||
UserKeyEncryptedAccountPrivateKey = _privateKey
|
||||
},
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
|
||||
|
||||
// Assert
|
||||
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToKeyConnectorKeysData_NullKeyConnectorKeyWrappedUserKey_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = null,
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = _publicKey,
|
||||
UserKeyEncryptedAccountPrivateKey = _privateKey
|
||||
},
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
|
||||
|
||||
// Assert
|
||||
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToKeyConnectorKeysData_NullAccountKeys_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
|
||||
AccountKeys = null,
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
|
||||
|
||||
// Assert
|
||||
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToKeyConnectorKeysData_Valid_Success()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SetKeyConnectorKeyRequestModel
|
||||
{
|
||||
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
|
||||
AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
AccountPublicKey = _publicKey,
|
||||
UserKeyEncryptedAccountPrivateKey = _privateKey
|
||||
},
|
||||
OrgIdentifier = _orgIdentifier
|
||||
};
|
||||
|
||||
// Act
|
||||
var data = model.ToKeyConnectorKeysData();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(_wrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
|
||||
Assert.Equal(_publicKey, data.AccountKeys.AccountPublicKey);
|
||||
Assert.Equal(_privateKey, data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
|
||||
Assert.Equal(_orgIdentifier, data.OrgIdentifier);
|
||||
}
|
||||
|
||||
private static List<ValidationResult> Validate(SetKeyConnectorKeyRequestModel model)
|
||||
{
|
||||
var results = new List<ValidationResult>();
|
||||
Validator.TryValidateObject(model, new ValidationContext(model), results, true);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Text.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Tools.Controllers;
|
||||
using Bit.Api.Tools.Models;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.Entities;
|
||||
@@ -12,6 +15,7 @@ using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -29,6 +33,7 @@ public class SendsControllerTests : IDisposable
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
|
||||
private readonly IAnonymousSendCommand _anonymousSendCommand;
|
||||
private readonly ISendOwnerQuery _sendOwnerQuery;
|
||||
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||
private readonly ISendFileStorageService _sendFileStorageService;
|
||||
private readonly ILogger<SendsController> _logger;
|
||||
@@ -39,6 +44,7 @@ public class SendsControllerTests : IDisposable
|
||||
_sendRepository = Substitute.For<ISendRepository>();
|
||||
_nonAnonymousSendCommand = Substitute.For<INonAnonymousSendCommand>();
|
||||
_anonymousSendCommand = Substitute.For<IAnonymousSendCommand>();
|
||||
_sendOwnerQuery = Substitute.For<ISendOwnerQuery>();
|
||||
_sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
|
||||
_sendFileStorageService = Substitute.For<ISendFileStorageService>();
|
||||
_globalSettings = new GlobalSettings();
|
||||
@@ -50,6 +56,7 @@ public class SendsControllerTests : IDisposable
|
||||
_sendAuthorizationService,
|
||||
_anonymousSendCommand,
|
||||
_nonAnonymousSendCommand,
|
||||
_sendOwnerQuery,
|
||||
_sendFileStorageService,
|
||||
_logger,
|
||||
_globalSettings
|
||||
@@ -109,4 +116,641 @@ public class SendsControllerTests : IDisposable
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostFile(request));
|
||||
Assert.Equal(expected, exception.Message);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Get_WithValidId_ReturnsSendResponseModel(Guid sendId, Send send)
|
||||
{
|
||||
send.Type = SendType.Text;
|
||||
var textData = new SendTextData("Test Send", "Notes", "Sample text", false);
|
||||
send.Data = JsonSerializer.Serialize(textData);
|
||||
_sendOwnerQuery.Get(sendId, Arg.Any<ClaimsPrincipal>()).Returns(send);
|
||||
|
||||
var result = await _sut.Get(sendId.ToString());
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<SendResponseModel>(result);
|
||||
Assert.Equal(send.Id, result.Id);
|
||||
await _sendOwnerQuery.Received(1).Get(sendId, Arg.Any<ClaimsPrincipal>());
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Get_WithInvalidGuid_ThrowsException(string invalidId)
|
||||
{
|
||||
await Assert.ThrowsAsync<FormatException>(() => _sut.Get(invalidId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllOwned_ReturnsListResponseModelWithSendResponseModels()
|
||||
{
|
||||
var textSendData = new SendTextData("Test Send 1", "Notes 1", "Sample text", false);
|
||||
var fileSendData = new SendFileData("Test Send 2", "Notes 2", "test.txt") { Id = "file-123", Size = 1024 };
|
||||
var sends = new List<Send>
|
||||
{
|
||||
new Send { Id = Guid.NewGuid(), Type = SendType.Text, Data = JsonSerializer.Serialize(textSendData) },
|
||||
new Send { Id = Guid.NewGuid(), Type = SendType.File, Data = JsonSerializer.Serialize(fileSendData) }
|
||||
};
|
||||
_sendOwnerQuery.GetOwned(Arg.Any<ClaimsPrincipal>()).Returns(sends);
|
||||
|
||||
var result = await _sut.GetAll();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<ListResponseModel<SendResponseModel>>(result);
|
||||
Assert.Equal(2, result.Data.Count());
|
||||
var sendResponseModels = result.Data.ToList();
|
||||
Assert.Equal(sends[0].Id, sendResponseModels[0].Id);
|
||||
Assert.Equal(sends[1].Id, sendResponseModels[1].Id);
|
||||
await _sendOwnerQuery.Received(1).GetOwned(Arg.Any<ClaimsPrincipal>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllOwned_WhenNoSends_ReturnsEmptyListResponseModel()
|
||||
{
|
||||
_sendOwnerQuery.GetOwned(Arg.Any<ClaimsPrincipal>()).Returns(new List<Send>());
|
||||
|
||||
var result = await _sut.GetAll();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<ListResponseModel<SendResponseModel>>(result);
|
||||
Assert.Empty(result.Data);
|
||||
await _sendOwnerQuery.Received(1).GetOwned(Arg.Any<ClaimsPrincipal>());
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Post_WithPassword_InfersAuthTypePassword(Guid userId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "key",
|
||||
Text = new SendTextModel { Text = "text" },
|
||||
Password = "password",
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.Post(request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(AuthType.Password, result.AuthType);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
|
||||
s.AuthType == AuthType.Password &&
|
||||
s.Password != null &&
|
||||
s.Emails == null &&
|
||||
s.UserId == userId &&
|
||||
s.Type == SendType.Text));
|
||||
_userService.Received(1).GetProperUserId(Arg.Any<ClaimsPrincipal>());
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Post_WithEmails_InfersAuthTypeEmail(Guid userId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "key",
|
||||
Text = new SendTextModel { Text = "text" },
|
||||
Emails = "test@example.com",
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.Post(request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(AuthType.Email, result.AuthType);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
|
||||
s.AuthType == AuthType.Email &&
|
||||
s.Emails != null &&
|
||||
s.Password == null &&
|
||||
s.UserId == userId &&
|
||||
s.Type == SendType.Text));
|
||||
_userService.Received(1).GetProperUserId(Arg.Any<ClaimsPrincipal>());
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Post_WithoutPasswordOrEmails_InfersAuthTypeNone(Guid userId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "key",
|
||||
Text = new SendTextModel { Text = "text" },
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.Post(request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(AuthType.None, result.AuthType);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
|
||||
s.AuthType == AuthType.None &&
|
||||
s.Password == null &&
|
||||
s.Emails == null &&
|
||||
s.UserId == userId &&
|
||||
s.Type == SendType.Text));
|
||||
_userService.Received(1).GetProperUserId(Arg.Any<ClaimsPrincipal>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(AuthType.Password)]
|
||||
[InlineData(AuthType.Email)]
|
||||
[InlineData(AuthType.None)]
|
||||
public async Task Access_ReturnsCorrectAuthType(AuthType authType)
|
||||
{
|
||||
var sendId = Guid.NewGuid();
|
||||
var accessId = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
|
||||
var send = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new Dictionary<string, string>()),
|
||||
AuthType = authType
|
||||
};
|
||||
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
_sendAuthorizationService.AccessAsync(send, "pwd123").Returns(SendAccessResult.Granted);
|
||||
|
||||
var request = new SendAccessRequestModel();
|
||||
var actionResult = await _sut.Access(accessId, request);
|
||||
var response = (actionResult as ObjectResult)?.Value as SendAccessResponseModel;
|
||||
|
||||
Assert.NotNull(response);
|
||||
Assert.Equal(authType, response.AuthType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(AuthType.Password)]
|
||||
[InlineData(AuthType.Email)]
|
||||
[InlineData(AuthType.None)]
|
||||
public async Task Get_ReturnsCorrectAuthType(AuthType authType)
|
||||
{
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("a", "b", "c", false)),
|
||||
AuthType = authType
|
||||
};
|
||||
|
||||
_sendOwnerQuery.Get(sendId, Arg.Any<ClaimsPrincipal>()).Returns(send);
|
||||
|
||||
var result = await _sut.Get(sendId.ToString());
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(authType, result.AuthType);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Put_WithValidSend_UpdatesSuccessfully(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = userId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
|
||||
AuthType = AuthType.None
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "updated-key",
|
||||
Text = new SendTextModel { Text = "updated text" },
|
||||
Password = "new-password",
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.Put(sendId.ToString(), request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(sendId, result.Id);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s => s.Id == sendId));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Put_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns((Send)null);
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "key",
|
||||
Text = new SendTextModel { Text = "text" },
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.Put(sendId.ToString(), request));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Put_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = otherUserId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false))
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "key",
|
||||
Text = new SendTextModel { Text = "text" },
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.Put(sendId.ToString(), request));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task PutRemovePassword_WithValidSend_RemovesPasswordAndSetsAuthTypeNone(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = userId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
|
||||
Password = "hashed-password",
|
||||
AuthType = AuthType.Password
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
var result = await _sut.PutRemovePassword(sendId.ToString());
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(sendId, result.Id);
|
||||
Assert.Equal(AuthType.None, result.AuthType);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
|
||||
s.Id == sendId &&
|
||||
s.Password == null &&
|
||||
s.AuthType == AuthType.None));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task PutRemovePassword_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns((Send)null);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PutRemovePassword(sendId.ToString()));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task PutRemovePassword_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = otherUserId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
|
||||
Password = "hashed-password"
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PutRemovePassword(sendId.ToString()));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Delete_WithValidSend_DeletesSuccessfully(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = userId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false))
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
await _sut.Delete(sendId.ToString());
|
||||
|
||||
await _nonAnonymousSendCommand.Received(1).DeleteSendAsync(Arg.Is<Send>(s => s.Id == sendId));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Delete_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns((Send)null);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.Delete(sendId.ToString()));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Delete_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = otherUserId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false))
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.Delete(sendId.ToString()));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task PostFile_WithPassword_InfersAuthTypePassword(Guid userId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
_nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>())
|
||||
.Returns("https://example.com/upload")
|
||||
.AndDoes(callInfo =>
|
||||
{
|
||||
var send = callInfo.ArgAt<Send>(0);
|
||||
var data = callInfo.ArgAt<SendFileData>(1);
|
||||
send.Data = JsonSerializer.Serialize(data);
|
||||
});
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.File,
|
||||
Key = "key",
|
||||
File = new SendFileModel { FileName = "test.txt" },
|
||||
FileLength = 1024L,
|
||||
Password = "password",
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.PostFile(request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.SendResponse);
|
||||
Assert.Equal(AuthType.Password, result.SendResponse.AuthType);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync(
|
||||
Arg.Is<Send>(s =>
|
||||
s.AuthType == AuthType.Password &&
|
||||
s.Password != null &&
|
||||
s.Emails == null &&
|
||||
s.UserId == userId),
|
||||
Arg.Any<SendFileData>(),
|
||||
1024L);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task PostFile_WithEmails_InfersAuthTypeEmail(Guid userId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
_nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>())
|
||||
.Returns("https://example.com/upload")
|
||||
.AndDoes(callInfo =>
|
||||
{
|
||||
var send = callInfo.ArgAt<Send>(0);
|
||||
var data = callInfo.ArgAt<SendFileData>(1);
|
||||
send.Data = JsonSerializer.Serialize(data);
|
||||
});
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.File,
|
||||
Key = "key",
|
||||
File = new SendFileModel { FileName = "test.txt" },
|
||||
FileLength = 1024L,
|
||||
Emails = "test@example.com",
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.PostFile(request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.SendResponse);
|
||||
Assert.Equal(AuthType.Email, result.SendResponse.AuthType);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync(
|
||||
Arg.Is<Send>(s =>
|
||||
s.AuthType == AuthType.Email &&
|
||||
s.Emails != null &&
|
||||
s.Password == null &&
|
||||
s.UserId == userId),
|
||||
Arg.Any<SendFileData>(),
|
||||
1024L);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task PostFile_WithoutPasswordOrEmails_InfersAuthTypeNone(Guid userId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
_nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>())
|
||||
.Returns("https://example.com/upload")
|
||||
.AndDoes(callInfo =>
|
||||
{
|
||||
var send = callInfo.ArgAt<Send>(0);
|
||||
var data = callInfo.ArgAt<SendFileData>(1);
|
||||
send.Data = JsonSerializer.Serialize(data);
|
||||
});
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.File,
|
||||
Key = "key",
|
||||
File = new SendFileModel { FileName = "test.txt" },
|
||||
FileLength = 1024L,
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.PostFile(request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.SendResponse);
|
||||
Assert.Equal(AuthType.None, result.SendResponse.AuthType);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync(
|
||||
Arg.Is<Send>(s =>
|
||||
s.AuthType == AuthType.None &&
|
||||
s.Password == null &&
|
||||
s.Emails == null &&
|
||||
s.UserId == userId),
|
||||
Arg.Any<SendFileData>(),
|
||||
1024L);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Put_ChangingFromPasswordToEmails_UpdatesAuthTypeToEmail(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = userId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
|
||||
Password = "hashed-password",
|
||||
AuthType = AuthType.Password
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "updated-key",
|
||||
Text = new SendTextModel { Text = "updated text" },
|
||||
Emails = "new@example.com",
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.Put(sendId.ToString(), request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(sendId, result.Id);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
|
||||
s.Id == sendId &&
|
||||
s.AuthType == AuthType.Email &&
|
||||
s.Emails != null &&
|
||||
s.Password == null));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Put_ChangingFromEmailToPassword_UpdatesAuthTypeToPassword(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = userId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
|
||||
Emails = "old@example.com",
|
||||
AuthType = AuthType.Email
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "updated-key",
|
||||
Text = new SendTextModel { Text = "updated text" },
|
||||
Password = "new-password",
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.Put(sendId.ToString(), request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(sendId, result.Id);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
|
||||
s.Id == sendId &&
|
||||
s.AuthType == AuthType.Password &&
|
||||
s.Password != null &&
|
||||
s.Emails == null));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Put_WithoutPasswordOrEmails_PreservesExistingPassword(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = userId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
|
||||
Password = "hashed-password",
|
||||
AuthType = AuthType.Password
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "updated-key",
|
||||
Text = new SendTextModel { Text = "updated text" },
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.Put(sendId.ToString(), request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(sendId, result.Id);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
|
||||
s.Id == sendId &&
|
||||
s.AuthType == AuthType.Password &&
|
||||
s.Password == "hashed-password" &&
|
||||
s.Emails == null));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Put_WithoutPasswordOrEmails_PreservesExistingEmails(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = userId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
|
||||
Emails = "test@example.com",
|
||||
AuthType = AuthType.Email
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "updated-key",
|
||||
Text = new SendTextModel { Text = "updated text" },
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.Put(sendId.ToString(), request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(sendId, result.Id);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
|
||||
s.Id == sendId &&
|
||||
s.AuthType == AuthType.Email &&
|
||||
s.Emails == "test@example.com" &&
|
||||
s.Password == null));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Put_WithoutPasswordOrEmails_PreservesNoneAuthType(Guid userId, Guid sendId)
|
||||
{
|
||||
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
var existingSend = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = userId,
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
|
||||
Password = null,
|
||||
Emails = null,
|
||||
AuthType = AuthType.None
|
||||
};
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
|
||||
|
||||
var request = new SendRequestModel
|
||||
{
|
||||
Type = SendType.Text,
|
||||
Key = "updated-key",
|
||||
Text = new SendTextModel { Text = "updated text" },
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var result = await _sut.Put(sendId.ToString(), request);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(sendId, result.Id);
|
||||
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
|
||||
s.Id == sendId &&
|
||||
s.AuthType == AuthType.None &&
|
||||
s.Password == null &&
|
||||
s.Emails == null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Api.Models.Public.Request;
|
||||
using Bit.Api.Dirt.Public.Models;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Api.Utilities.DiagnosticTools;
|
||||
using Bit.Core;
|
||||
@@ -155,7 +155,7 @@ public class EventDiagnosticLoggerTests
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true);
|
||||
|
||||
Bit.Api.Models.Response.EventResponseModel[] emptyEvents = [];
|
||||
Api.Dirt.Models.Response.EventResponseModel[] emptyEvents = [];
|
||||
|
||||
// Act
|
||||
logger.LogAggregateData(featureService, organizationId, emptyEvents, null, null, null);
|
||||
@@ -188,7 +188,7 @@ public class EventDiagnosticLoggerTests
|
||||
var oldestEvent = Substitute.For<IEvent>();
|
||||
oldestEvent.Date.Returns(DateTime.UtcNow.AddDays(-2));
|
||||
|
||||
var events = new List<Bit.Api.Models.Response.EventResponseModel>
|
||||
var events = new List<Api.Dirt.Models.Response.EventResponseModel>
|
||||
{
|
||||
new (newestEvent),
|
||||
new (middleEvent),
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Billing.Controllers;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReceivedExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Billing.Test.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(FreshdeskController))]
|
||||
[SutProviderCustomize]
|
||||
public class FreshdeskControllerTests
|
||||
{
|
||||
private const string ApiKey = "TESTFRESHDESKAPIKEY";
|
||||
private const string WebhookKey = "TESTKEY";
|
||||
|
||||
private const string UserFieldName = "cf_user";
|
||||
private const string OrgFieldName = "cf_org";
|
||||
|
||||
[Theory]
|
||||
[BitAutoData((string)null, null)]
|
||||
[BitAutoData((string)null)]
|
||||
[BitAutoData(WebhookKey, null)]
|
||||
public async Task PostWebhook_NullRequiredParameters_BadRequest(string freshdeskWebhookKey, FreshdeskWebhookModel model,
|
||||
BillingSettings billingSettings, SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>().Value.FreshDesk.WebhookKey.Returns(billingSettings.FreshDesk.WebhookKey);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhook(freshdeskWebhookKey, model);
|
||||
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, statusCodeResult.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostWebhook_Success(User user, FreshdeskWebhookModel model,
|
||||
List<Organization> organizations, SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
model.TicketContactEmail = user.Email;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(user.Email).Returns(user);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetManyByUserIdAsync(user.Id).Returns(organizations);
|
||||
|
||||
var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
|
||||
mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockResponse);
|
||||
var httpClient = new HttpClient(mockHttpMessageHandler);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(httpClient);
|
||||
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>().Value.FreshDesk.WebhookKey.Returns(WebhookKey);
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>().Value.FreshDesk.ApiKey.Returns(ApiKey);
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>().Value.FreshDesk.UserFieldName.Returns(UserFieldName);
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>().Value.FreshDesk.OrgFieldName.Returns(OrgFieldName);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhook(WebhookKey, model);
|
||||
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode);
|
||||
|
||||
_ = mockHttpMessageHandler.Received(1).Send(Arg.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Put && m.RequestUri.ToString().EndsWith(model.TicketId)), Arg.Any<CancellationToken>());
|
||||
_ = mockHttpMessageHandler.Received(1).Send(Arg.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Post && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes")), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhook_add_note_when_user_is_invalid(
|
||||
string freshdeskWebhookKey, FreshdeskWebhookModel model,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
// Arrange - for an invalid user
|
||||
model.TicketContactEmail = "invalid@user";
|
||||
sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(model.TicketContactEmail).Returns((User)null);
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>().Value.FreshDesk.WebhookKey.Returns(WebhookKey);
|
||||
|
||||
var mockHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
|
||||
mockHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockResponse);
|
||||
var httpClient = new HttpClient(mockHttpMessageHandler);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(httpClient);
|
||||
|
||||
// Act
|
||||
var response = await sutProvider.Sut.PostWebhook(freshdeskWebhookKey, model);
|
||||
|
||||
// Assert
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode);
|
||||
|
||||
await mockHttpMessageHandler
|
||||
.Received(1).Send(
|
||||
Arg.Is<HttpRequestMessage>(
|
||||
m => m.Method == HttpMethod.Post
|
||||
&& m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes")
|
||||
&& m.Content.ReadAsStringAsync().Result.Contains("No user found")),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[BitAutoData((string)null, null)]
|
||||
[BitAutoData((string)null)]
|
||||
[BitAutoData(WebhookKey, null)]
|
||||
public async Task PostWebhookOnyxAi_InvalidWebhookKey_results_in_BadRequest(
|
||||
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
|
||||
BillingSettings billingSettings, SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOptions<BillingSettings>>()
|
||||
.Value.FreshDesk.WebhookKey.Returns(billingSettings.FreshDesk.WebhookKey);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, statusCodeResult.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_invalid_onyx_response_results_is_logged(
|
||||
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
|
||||
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
|
||||
billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api");
|
||||
|
||||
// mocking freshdesk Api request for ticket info
|
||||
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
|
||||
|
||||
// mocking Onyx api response given a ticket description
|
||||
var mockOnyxHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
|
||||
mockOnyxHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockOnyxResponse);
|
||||
var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("OnyxApi").Returns(onyxHttpClient);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode);
|
||||
|
||||
var _logger = sutProvider.GetDependency<ILogger<FreshdeskController>>();
|
||||
|
||||
// workaround because _logger.Received(1).LogWarning(...) does not work
|
||||
_logger.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Log" && c.GetArguments()[1].ToString().Contains("Error getting answer from Onyx AI"));
|
||||
|
||||
// sent call to Onyx API - but we got an error response
|
||||
_ = mockOnyxHttpMessageHandler.Received(1).Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
|
||||
// did not call freshdesk to add a note since onyx failed
|
||||
_ = mockFreshdeskHttpMessageHandler.DidNotReceive().Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_success(
|
||||
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
|
||||
OnyxResponseModel onyxResponse,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
|
||||
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
|
||||
billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api");
|
||||
|
||||
// mocking freshdesk api add note request (POST)
|
||||
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var mockFreshdeskAddNoteResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
|
||||
mockFreshdeskHttpMessageHandler.Send(
|
||||
Arg.Is<HttpRequestMessage>(_ => _.Method == HttpMethod.Post),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(mockFreshdeskAddNoteResponse);
|
||||
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
|
||||
|
||||
// mocking Onyx api response given a ticket description
|
||||
var mockOnyxHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
onyxResponse.ErrorMsg = "string.Empty";
|
||||
var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(onyxResponse))
|
||||
};
|
||||
mockOnyxHttpMessageHandler.Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockOnyxResponse);
|
||||
var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("OnyxApi").Returns(onyxHttpClient);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var result = Assert.IsAssignableFrom<OkResult>(response);
|
||||
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_ticket_description_is_empty_return_success(
|
||||
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
|
||||
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
|
||||
billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api");
|
||||
|
||||
model.TicketDescriptionText = " "; // empty description
|
||||
|
||||
// mocking freshdesk api add note request (POST)
|
||||
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
|
||||
|
||||
// mocking Onyx api response given a ticket description
|
||||
var mockOnyxHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("OnyxApi").Returns(onyxHttpClient);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var result = Assert.IsAssignableFrom<OkResult>(response);
|
||||
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
|
||||
_ = mockFreshdeskHttpMessageHandler.DidNotReceive().Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
|
||||
_ = mockOnyxHttpMessageHandler.DidNotReceive().Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
public class MockHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
return Send(request, cancellationToken);
|
||||
}
|
||||
|
||||
public new virtual Task<HttpResponseMessage> Send(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
using Bit.Billing.Controllers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Billing.Test.Controllers;
|
||||
|
||||
public class FreshsalesControllerTests
|
||||
{
|
||||
private const string ApiKey = "TEST_FRESHSALES_APIKEY";
|
||||
private const string TestLead = "TEST_FRESHSALES_TESTLEAD";
|
||||
|
||||
private static (FreshsalesController, IUserRepository, IOrganizationRepository) CreateSut(
|
||||
string freshsalesApiKey)
|
||||
{
|
||||
var userRepository = Substitute.For<IUserRepository>();
|
||||
var organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
|
||||
var billingSettings = Options.Create(new BillingSettings
|
||||
{
|
||||
FreshsalesApiKey = freshsalesApiKey,
|
||||
});
|
||||
var globalSettings = new GlobalSettings();
|
||||
globalSettings.BaseServiceUri.Admin = "https://test.com";
|
||||
|
||||
var sut = new FreshsalesController(
|
||||
userRepository,
|
||||
organizationRepository,
|
||||
billingSettings,
|
||||
Substitute.For<ILogger<FreshsalesController>>(),
|
||||
globalSettings
|
||||
);
|
||||
|
||||
return (sut, userRepository, organizationRepository);
|
||||
}
|
||||
|
||||
[RequiredEnvironmentTheory(ApiKey, TestLead), EnvironmentData(ApiKey, TestLead)]
|
||||
public async Task PostWebhook_Success(string freshsalesApiKey, long leadId)
|
||||
{
|
||||
// This test is only for development to use:
|
||||
// `export TEST_FRESHSALES_APIKEY=[apikey]`
|
||||
// `export TEST_FRESHSALES_TESTLEAD=[lead id]`
|
||||
// `dotnet test --filter "FullyQualifiedName~FreshsalesControllerTests.PostWebhook_Success"`
|
||||
var (sut, userRepository, organizationRepository) = CreateSut(freshsalesApiKey);
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = "test@email.com",
|
||||
Premium = true,
|
||||
};
|
||||
|
||||
userRepository.GetByEmailAsync(user.Email)
|
||||
.Returns(user);
|
||||
|
||||
organizationRepository.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<Organization>
|
||||
{
|
||||
new Organization
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Test Org",
|
||||
}
|
||||
});
|
||||
|
||||
var response = await sut.PostWebhook(freshsalesApiKey, new CustomWebhookRequestModel
|
||||
{
|
||||
LeadId = leadId,
|
||||
}, new CancellationToken(false));
|
||||
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status204NoContent, statusCodeResult.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
@@ -17,6 +18,9 @@ public class ReconcileAdditionalStorageJobTests
|
||||
private readonly IStripeFacade _stripeFacade;
|
||||
private readonly ILogger<ReconcileAdditionalStorageJob> _logger;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||
private readonly ReconcileAdditionalStorageJob _sut;
|
||||
|
||||
public ReconcileAdditionalStorageJobTests()
|
||||
@@ -24,7 +28,20 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_stripeFacade = Substitute.For<IStripeFacade>();
|
||||
_logger = Substitute.For<ILogger<ReconcileAdditionalStorageJob>>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_sut = new ReconcileAdditionalStorageJob(_stripeFacade, _logger, _featureService);
|
||||
_userRepository = Substitute.For<IUserRepository>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_stripeEventUtilityService = Substitute.For<IStripeEventUtilityService>();
|
||||
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, null));
|
||||
|
||||
_sut = new ReconcileAdditionalStorageJob(
|
||||
_stripeFacade,
|
||||
_logger,
|
||||
_featureService,
|
||||
_userRepository,
|
||||
_organizationRepository,
|
||||
_stripeEventUtilityService);
|
||||
}
|
||||
|
||||
#region Feature Flag Tests
|
||||
@@ -62,7 +79,7 @@ public class ReconcileAdditionalStorageJobTests
|
||||
|
||||
// Assert
|
||||
_stripeFacade.Received(3).ListSubscriptionsAutoPagingAsync(
|
||||
Arg.Is<SubscriptionListOptions>(o => o.Status == "active"));
|
||||
Arg.Is<SubscriptionListOptions>(o => o.Limit == 100));
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -88,6 +105,36 @@ public class ReconcileAdditionalStorageJobTests
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_DryRunMode_DoesNotUpdateDatabase()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(false); // Dry run ON
|
||||
|
||||
// Create a personal subscription that would normally trigger a database update
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
subscription.Metadata = new Dictionary<string, string> { ["userId"] = userId.ToString() };
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
|
||||
// Mock GetIdsFromMetadata to return userId
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert - Verify database repositories are never called
|
||||
await _userRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default);
|
||||
await _userRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default!);
|
||||
await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default);
|
||||
await _organizationRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_DryRunModeDisabled_UpdatesSubscriptions()
|
||||
{
|
||||
@@ -96,7 +143,11 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true); // Dry run OFF
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
@@ -111,6 +162,150 @@ public class ReconcileAdditionalStorageJobTests
|
||||
Arg.Is<SubscriptionUpdateOptions>(o => o.Items.Count == 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LiveMode_PersonalSubscription_UpdatesUserDatabase()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
// Setup user
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new Bit.Core.Entities.User
|
||||
{
|
||||
Id = userId,
|
||||
Email = "test@example.com",
|
||||
GatewaySubscriptionId = "sub_personal",
|
||||
MaxStorageGb = 15 // Old value
|
||||
};
|
||||
_userRepository.GetByIdAsync(userId).Returns(user);
|
||||
_userRepository.ReplaceAsync(user).Returns(Task.CompletedTask);
|
||||
|
||||
// Create personal subscription with premium seat + 10 GB storage (will be reduced to 6 GB)
|
||||
var subscription = CreateSubscriptionWithMultipleItems("sub_personal",
|
||||
[("premium-annually", 1L), ("storage-gb-monthly", 10L)]);
|
||||
subscription.Metadata = new Dictionary<string, string> { ["userId"] = userId.ToString() };
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Mock GetIdsFromMetadata to return userId
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert - Verify Stripe update happened
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
"sub_personal",
|
||||
Arg.Is<SubscriptionUpdateOptions>(o => o.Items.Count == 1 && o.Items[0].Quantity == 6));
|
||||
|
||||
// Assert - Verify database update with correct MaxStorageGb (5 included + 6 new quantity = 11)
|
||||
await _userRepository.Received(1).GetByIdAsync(userId);
|
||||
await _userRepository.Received(1).ReplaceAsync(user);
|
||||
Assert.Equal((short)11, user.MaxStorageGb);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LiveMode_OrganizationSubscription_UpdatesOrganizationDatabase()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
// Setup organization
|
||||
var organizationId = Guid.NewGuid();
|
||||
var organization = new Bit.Core.AdminConsole.Entities.Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
Name = "Test Organization",
|
||||
GatewaySubscriptionId = "sub_org",
|
||||
MaxStorageGb = 13 // Old value
|
||||
};
|
||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||
_organizationRepository.ReplaceAsync(organization).Returns(Task.CompletedTask);
|
||||
|
||||
// Create organization subscription with org seat plan + 8 GB storage (will be reduced to 4 GB)
|
||||
var subscription = CreateSubscriptionWithMultipleItems("sub_org",
|
||||
[("2023-teams-org-seat-annually", 5L), ("storage-gb-monthly", 8L)]);
|
||||
subscription.Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() };
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Mock GetIdsFromMetadata to return organizationId
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(organizationId, null, null));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert - Verify Stripe update happened
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
"sub_org",
|
||||
Arg.Is<SubscriptionUpdateOptions>(o => o.Items.Count == 1 && o.Items[0].Quantity == 4));
|
||||
|
||||
// Assert - Verify database update with correct MaxStorageGb (5 included + 4 new quantity = 9)
|
||||
await _organizationRepository.Received(1).GetByIdAsync(organizationId);
|
||||
await _organizationRepository.Received(1).ReplaceAsync(organization);
|
||||
Assert.Equal((short)9, organization.MaxStorageGb);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_LiveMode_StorageItemDeleted_UpdatesDatabaseWithBaseStorage()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
// Setup user
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new Bit.Core.Entities.User
|
||||
{
|
||||
Id = userId,
|
||||
Email = "test@example.com",
|
||||
GatewaySubscriptionId = "sub_delete",
|
||||
MaxStorageGb = 8 // Old value
|
||||
};
|
||||
_userRepository.GetByIdAsync(userId).Returns(user);
|
||||
_userRepository.ReplaceAsync(user).Returns(Task.CompletedTask);
|
||||
|
||||
// Create personal subscription with premium seat + 3 GB storage (will be deleted since 3 < 4)
|
||||
var subscription = CreateSubscriptionWithMultipleItems("sub_delete",
|
||||
[("premium-annually", 1L), ("storage-gb-monthly", 3L)]);
|
||||
subscription.Metadata = new Dictionary<string, string> { ["userId"] = userId.ToString() };
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Mock GetIdsFromMetadata to return userId
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert - Verify Stripe update happened (item deleted)
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
"sub_delete",
|
||||
Arg.Is<SubscriptionUpdateOptions>(o => o.Items.Count == 1 && o.Items[0].Deleted == true));
|
||||
|
||||
// Assert - Verify database update with base storage only (5 GB)
|
||||
await _userRepository.Received(1).GetByIdAsync(userId);
|
||||
await _userRepository.Received(1).ReplaceAsync(user);
|
||||
Assert.Equal((short)5, user.MaxStorageGb);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Price ID Processing Tests
|
||||
@@ -174,11 +369,14 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
[StripeConstants.MetadataKeys.StorageReconciled2025] = "invalid-date"
|
||||
};
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, metadata: metadata);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
@@ -200,7 +398,10 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, metadata: null);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
@@ -226,7 +427,10 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
@@ -253,7 +457,10 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 4);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
@@ -279,7 +486,10 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 2);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
@@ -309,7 +519,10 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
@@ -333,7 +546,10 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
@@ -429,9 +645,12 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription1 = CreateSubscription("sub_1", "storage-gb-monthly", quantity: 10);
|
||||
var subscription2 = CreateSubscription("sub_2", "storage-gb-monthly", quantity: 5);
|
||||
var subscription3 = CreateSubscription("sub_3", "storage-gb-monthly", quantity: 3);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription1, subscription2, subscription3));
|
||||
@@ -461,6 +680,7 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var processedMetadata = new Dictionary<string, string>
|
||||
{
|
||||
[StripeConstants.MetadataKeys.StorageReconciled2025] = DateTime.UtcNow.ToString("o")
|
||||
@@ -469,6 +689,8 @@ public class ReconcileAdditionalStorageJobTests
|
||||
var subscription1 = CreateSubscription("sub_1", "storage-gb-monthly", quantity: 10);
|
||||
var subscription2 = CreateSubscription("sub_2", "storage-gb-monthly", quantity: 5, metadata: processedMetadata);
|
||||
var subscription3 = CreateSubscription("sub_3", "storage-gb-monthly", quantity: 3);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription1, subscription2, subscription3));
|
||||
@@ -501,9 +723,12 @@ public class ReconcileAdditionalStorageJobTests
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription1 = CreateSubscription("sub_1", "storage-gb-monthly", quantity: 10);
|
||||
var subscription2 = CreateSubscription("sub_2", "storage-gb-monthly", quantity: 5);
|
||||
var subscription3 = CreateSubscription("sub_3", "storage-gb-monthly", quantity: 3);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription1, subscription2, subscription3));
|
||||
@@ -553,6 +778,164 @@ public class ReconcileAdditionalStorageJobTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Subscription Status Filtering Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_ActiveStatusSubscription_ProcessesSubscription()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.Active);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_123", Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_TrialingStatusSubscription_ProcessesSubscription()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.Trialing);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_123", Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_PastDueStatusSubscription_ProcessesSubscription()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.PastDue);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_123", Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_CanceledStatusSubscription_SkipsSubscription()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.Canceled);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_IncompleteStatusSubscription_SkipsSubscription()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.Incomplete);
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(subscription));
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Execute_MixedSubscriptionStatuses_OnlyProcessesValidStatuses()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateJobExecutionContext();
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var activeSubscription = CreateSubscription("sub_active", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.Active);
|
||||
var trialingSubscription = CreateSubscription("sub_trialing", "storage-gb-monthly", quantity: 8, status: StripeConstants.SubscriptionStatus.Trialing);
|
||||
var pastDueSubscription = CreateSubscription("sub_pastdue", "storage-gb-monthly", quantity: 6, status: StripeConstants.SubscriptionStatus.PastDue);
|
||||
var canceledSubscription = CreateSubscription("sub_canceled", "storage-gb-monthly", quantity: 5, status: StripeConstants.SubscriptionStatus.Canceled);
|
||||
var incompleteSubscription = CreateSubscription("sub_incomplete", "storage-gb-monthly", quantity: 4, status: StripeConstants.SubscriptionStatus.Incomplete);
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())
|
||||
.Returns(AsyncEnumerable.Create(activeSubscription, trialingSubscription, pastDueSubscription, canceledSubscription, incompleteSubscription));
|
||||
_stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(callInfo => callInfo.Arg<string>() switch
|
||||
{
|
||||
"sub_active" => activeSubscription,
|
||||
"sub_trialing" => trialingSubscription,
|
||||
"sub_pastdue" => pastDueSubscription,
|
||||
_ => null
|
||||
});
|
||||
|
||||
// Act
|
||||
await _sut.Execute(context);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_active", Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_trialing", Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _stripeFacade.Received(1).UpdateSubscription("sub_pastdue", Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _stripeFacade.DidNotReceive().UpdateSubscription("sub_canceled", Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _stripeFacade.DidNotReceive().UpdateSubscription("sub_incomplete", Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cancellation Tests
|
||||
|
||||
[Fact]
|
||||
@@ -585,6 +968,410 @@ public class ReconcileAdditionalStorageJobTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Method Tests
|
||||
|
||||
#region DetermineSubscriptionPlanTier Tests
|
||||
|
||||
[Fact]
|
||||
public void DetermineSubscriptionPlanTier_WithUserId_ReturnsPersonal()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
Guid? organizationId = null;
|
||||
|
||||
// Act
|
||||
var result = _sut.DetermineSubscriptionPlanTier(userId, organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ReconcileAdditionalStorageJob.SubscriptionPlanTier.Personal, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineSubscriptionPlanTier_WithOrganizationId_ReturnsOrganization()
|
||||
{
|
||||
// Arrange
|
||||
Guid? userId = null;
|
||||
var organizationId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var result = _sut.DetermineSubscriptionPlanTier(userId, organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ReconcileAdditionalStorageJob.SubscriptionPlanTier.Organization, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineSubscriptionPlanTier_WithBothIds_ReturnsPersonal()
|
||||
{
|
||||
// Arrange - Personal takes precedence
|
||||
var userId = Guid.NewGuid();
|
||||
var organizationId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var result = _sut.DetermineSubscriptionPlanTier(userId, organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ReconcileAdditionalStorageJob.SubscriptionPlanTier.Personal, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineSubscriptionPlanTier_WithNoIds_ReturnsUnknown()
|
||||
{
|
||||
// Arrange
|
||||
Guid? userId = null;
|
||||
Guid? organizationId = null;
|
||||
|
||||
// Act
|
||||
var result = _sut.DetermineSubscriptionPlanTier(userId, organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ReconcileAdditionalStorageJob.SubscriptionPlanTier.Unknown, result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetCurrentStorageQuantityFromSubscription Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData("storage-gb-monthly", 10L, 10L)]
|
||||
[InlineData("storage-gb-annually", 25L, 25L)]
|
||||
[InlineData("personal-storage-gb-annually", 5L, 5L)]
|
||||
[InlineData("storage-gb-monthly", 0L, 0L)]
|
||||
public void GetCurrentStorageQuantityFromSubscription_WithMatchingPriceId_ReturnsQuantity(
|
||||
string priceId, long quantity, long expectedQuantity)
|
||||
{
|
||||
// Arrange
|
||||
var subscription = CreateSubscription("sub_123", priceId, quantity);
|
||||
|
||||
// Act
|
||||
var result = _sut.GetCurrentStorageQuantityFromSubscription(subscription, priceId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedQuantity, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCurrentStorageQuantityFromSubscription_WithNonMatchingPriceId_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
var subscription = CreateSubscription("sub_123", "storage-gb-monthly", 10L);
|
||||
|
||||
// Act
|
||||
var result = _sut.GetCurrentStorageQuantityFromSubscription(subscription, "different-price-id");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCurrentStorageQuantityFromSubscription_WithNullItems_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
var subscription = new Subscription { Id = "sub_123", Items = null };
|
||||
|
||||
// Act
|
||||
var result = _sut.GetCurrentStorageQuantityFromSubscription(subscription, "storage-gb-monthly");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCurrentStorageQuantityFromSubscription_WithEmptyItems_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
Items = new StripeList<SubscriptionItem> { Data = [] }
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.GetCurrentStorageQuantityFromSubscription(subscription, "storage-gb-monthly");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CalculateNewMaxStorageGb Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(10L, 6L, 11)] // 5 included + 6 new quantity
|
||||
[InlineData(15L, 11L, 16)] // 5 included + 11 new quantity
|
||||
[InlineData(4L, 0L, 5)] // Item deleted, returns base storage
|
||||
[InlineData(2L, 0L, 5)] // Item deleted, returns base storage
|
||||
[InlineData(8L, 4L, 9)] // 5 included + 4 new quantity
|
||||
public void CalculateNewMaxStorageGb_WithQuantityUpdate_ReturnsCorrectMaxStorage(
|
||||
long currentQuantity, long newQuantity, short expectedMaxStorageGb)
|
||||
{
|
||||
// Arrange
|
||||
var updateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
newQuantity == 0
|
||||
? new SubscriptionItemOptions { Id = "si_123", Deleted = true } // Item marked as deleted
|
||||
: new SubscriptionItemOptions { Id = "si_123", Quantity = newQuantity } // Item quantity updated
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.CalculateNewMaxStorageGb(currentQuantity, updateOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedMaxStorageGb, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNewMaxStorageGb_WithNullUpdateOptions_ReturnsCurrentQuantityPlusBaseIncluded()
|
||||
{
|
||||
// Arrange
|
||||
const long currentQuantity = 10;
|
||||
|
||||
// Act
|
||||
var result = _sut.CalculateNewMaxStorageGb(currentQuantity, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal((short)(5 + currentQuantity), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNewMaxStorageGb_WithNullItems_ReturnsCurrentQuantityPlusBaseIncluded()
|
||||
{
|
||||
// Arrange
|
||||
const long currentQuantity = 10;
|
||||
var updateOptions = new SubscriptionUpdateOptions { Items = null };
|
||||
|
||||
// Act
|
||||
var result = _sut.CalculateNewMaxStorageGb(currentQuantity, updateOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5 + currentQuantity, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNewMaxStorageGb_WithEmptyItems_ReturnsCurrentQuantity()
|
||||
{
|
||||
// Arrange
|
||||
const long currentQuantity = 10;
|
||||
var updateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.CalculateNewMaxStorageGb(currentQuantity, updateOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5 + currentQuantity, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNewMaxStorageGb_WithDeletedItem_ReturnsBaseStorage()
|
||||
{
|
||||
// Arrange
|
||||
const long currentQuantity = 100;
|
||||
var updateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = [new SubscriptionItemOptions { Id = "si_123", Deleted = true }]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.CalculateNewMaxStorageGb(currentQuantity, updateOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Equal((short)5, result); // Base storage
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNewMaxStorageGb_WithItemWithoutQuantity_ReturnsCurrentQuantity()
|
||||
{
|
||||
// Arrange
|
||||
const long currentQuantity = 10;
|
||||
var updateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = [new SubscriptionItemOptions { Id = "si_123", Quantity = null }]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.CalculateNewMaxStorageGb(currentQuantity, updateOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5 + currentQuantity, result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateDatabaseMaxStorageAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDatabaseMaxStorageAsync_PersonalTier_UpdatesUser()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new Bit.Core.Entities.User
|
||||
{
|
||||
Id = userId,
|
||||
Email = "test@example.com",
|
||||
GatewaySubscriptionId = "sub_123"
|
||||
};
|
||||
_userRepository.GetByIdAsync(userId).Returns(user);
|
||||
_userRepository.ReplaceAsync(user).Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateDatabaseMaxStorageAsync(
|
||||
ReconcileAdditionalStorageJob.SubscriptionPlanTier.Personal,
|
||||
userId,
|
||||
10,
|
||||
"sub_123");
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Equal((short)10, user.MaxStorageGb);
|
||||
await _userRepository.Received(1).GetByIdAsync(userId);
|
||||
await _userRepository.Received(1).ReplaceAsync(user);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDatabaseMaxStorageAsync_PersonalTier_UserNotFound_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
_userRepository.GetByIdAsync(userId).Returns((Bit.Core.Entities.User?)null);
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateDatabaseMaxStorageAsync(
|
||||
ReconcileAdditionalStorageJob.SubscriptionPlanTier.Personal,
|
||||
userId,
|
||||
10,
|
||||
"sub_123");
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
await _userRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDatabaseMaxStorageAsync_PersonalTier_ReplaceThrowsException_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new Bit.Core.Entities.User
|
||||
{
|
||||
Id = userId,
|
||||
Email = "test@example.com",
|
||||
GatewaySubscriptionId = "sub_123"
|
||||
};
|
||||
_userRepository.GetByIdAsync(userId).Returns(user);
|
||||
_userRepository.ReplaceAsync(user).Throws(new Exception("Database error"));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateDatabaseMaxStorageAsync(
|
||||
ReconcileAdditionalStorageJob.SubscriptionPlanTier.Personal,
|
||||
userId,
|
||||
10,
|
||||
"sub_123");
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDatabaseMaxStorageAsync_OrganizationTier_UpdatesOrganization()
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var organization = new Bit.Core.AdminConsole.Entities.Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
Name = "Test Org",
|
||||
GatewaySubscriptionId = "sub_456"
|
||||
};
|
||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||
_organizationRepository.ReplaceAsync(organization).Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateDatabaseMaxStorageAsync(
|
||||
ReconcileAdditionalStorageJob.SubscriptionPlanTier.Organization,
|
||||
organizationId,
|
||||
20,
|
||||
"sub_456");
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Equal((short)20, organization.MaxStorageGb);
|
||||
await _organizationRepository.Received(1).GetByIdAsync(organizationId);
|
||||
await _organizationRepository.Received(1).ReplaceAsync(organization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDatabaseMaxStorageAsync_OrganizationTier_OrganizationNotFound_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
_organizationRepository.GetByIdAsync(organizationId)
|
||||
.Returns((Bit.Core.AdminConsole.Entities.Organization?)null);
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateDatabaseMaxStorageAsync(
|
||||
ReconcileAdditionalStorageJob.SubscriptionPlanTier.Organization,
|
||||
organizationId,
|
||||
20,
|
||||
"sub_456");
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
await _organizationRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDatabaseMaxStorageAsync_OrganizationTier_ReplaceThrowsException_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var organization = new Bit.Core.AdminConsole.Entities.Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
Name = "Test Org",
|
||||
GatewaySubscriptionId = "sub_456"
|
||||
};
|
||||
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||
_organizationRepository.ReplaceAsync(organization).Throws(new Exception("Database error"));
|
||||
|
||||
// Act
|
||||
var result = await _sut.UpdateDatabaseMaxStorageAsync(
|
||||
ReconcileAdditionalStorageJob.SubscriptionPlanTier.Organization,
|
||||
organizationId,
|
||||
20,
|
||||
"sub_456");
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDatabaseMaxStorageAsync_UnknownTier_ReturnsFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
var entityId = Guid.NewGuid();
|
||||
var result = await _sut.UpdateDatabaseMaxStorageAsync(
|
||||
ReconcileAdditionalStorageJob.SubscriptionPlanTier.Unknown,
|
||||
entityId,
|
||||
15,
|
||||
"sub_789");
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
await _userRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default);
|
||||
await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static IJobExecutionContext CreateJobExecutionContext(CancellationToken cancellationToken = default)
|
||||
@@ -598,7 +1385,8 @@ public class ReconcileAdditionalStorageJobTests
|
||||
string id,
|
||||
string priceId,
|
||||
long? quantity = null,
|
||||
Dictionary<string, string>? metadata = null)
|
||||
Dictionary<string, string>? metadata = null,
|
||||
string status = StripeConstants.SubscriptionStatus.Active)
|
||||
{
|
||||
var price = new Price { Id = priceId };
|
||||
var item = new SubscriptionItem
|
||||
@@ -611,10 +1399,31 @@ public class ReconcileAdditionalStorageJobTests
|
||||
return new Subscription
|
||||
{
|
||||
Id = id,
|
||||
Status = status,
|
||||
Metadata = metadata,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem> { item }
|
||||
Data = [item]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Subscription CreateSubscriptionWithMultipleItems(string id, (string priceId, long quantity)[] items)
|
||||
{
|
||||
var subscriptionItems = items.Select(i => new SubscriptionItem
|
||||
{
|
||||
Id = $"si_{id}_{i.priceId}",
|
||||
Price = new Price { Id = i.priceId },
|
||||
Quantity = i.quantity
|
||||
}).ToList();
|
||||
|
||||
return new Subscription
|
||||
{
|
||||
Id = id,
|
||||
Status = StripeConstants.SubscriptionStatus.Active,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = subscriptionItems
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,4 +96,23 @@ public class OrganizationTests
|
||||
var host = Assert.Contains("Host", (IDictionary<string, object>)duo.MetaData);
|
||||
Assert.Equal("Host_value", host);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseDisableSmAdsForUsers_DefaultValue_IsFalse()
|
||||
{
|
||||
var organization = new Organization();
|
||||
|
||||
Assert.False(organization.UseDisableSmAdsForUsers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseDisableSmAdsForUsers_CanBeSetToTrue()
|
||||
{
|
||||
var organization = new Organization
|
||||
{
|
||||
UseDisableSmAdsForUsers = true
|
||||
};
|
||||
|
||||
Assert.True(organization.UseDisableSmAdsForUsers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
@@ -727,4 +728,54 @@ public class AutomaticallyConfirmUsersCommandTests
|
||||
Arg.Any<IEnumerable<string>>(),
|
||||
organization.Id.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOn_UsesNewMailer(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const bool accessSecretsManager = true;
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(organization, userEmail, accessSecretsManager);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
|
||||
.Received(1)
|
||||
.SendConfirmationAsync(organization, userEmail, accessSecretsManager);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendOrganizationConfirmedEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOff_UsesLegacyMailService(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const bool accessSecretsManager = false;
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(organization, userEmail, accessSecretsManager);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationConfirmedEmailAsync(organization.Name, userEmail, accessSecretsManager);
|
||||
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
|
||||
.DidNotReceive()
|
||||
.SendConfirmationAsync(Arg.Any<Organization>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
@@ -462,7 +463,7 @@ public class ConfirmOrganizationUserCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection(
|
||||
public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection(
|
||||
Organization organization, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
@@ -475,8 +476,6 @@ public class ConfirmOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||
|
||||
var policyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
@@ -506,7 +505,7 @@ public class ConfirmOrganizationUserCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection(
|
||||
public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
@@ -519,8 +518,6 @@ public class ConfirmOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, "");
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
@@ -529,7 +526,7 @@ public class ConfirmOrganizationUserCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(
|
||||
public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted, OrganizationUserType.Owner)] OrganizationUser orgUser, User user,
|
||||
string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
@@ -541,7 +538,6 @@ public class ConfirmOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||
|
||||
var policyDetails = new PolicyDetails
|
||||
{
|
||||
@@ -814,4 +810,52 @@ public class ConfirmOrganizationUserCommandTests
|
||||
Assert.Empty(result[1].Item2);
|
||||
Assert.Equal(new OtherOrganizationDoesNotAllowOtherMembership().Message, result[2].Item2);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOn_UsesNewMailer(
|
||||
Organization org,
|
||||
string userEmail,
|
||||
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const bool accessSecretsManager = true;
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(org, userEmail, accessSecretsManager);
|
||||
|
||||
// Assert - verify new mailer is called, not legacy mail service
|
||||
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
|
||||
.Received(1)
|
||||
.SendConfirmationAsync(org, userEmail, accessSecretsManager);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.DidNotReceive()
|
||||
.SendOrganizationConfirmedEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SendOrganizationConfirmedEmailAsync_WithFeatureFlagOff_UsesLegacyMailService(
|
||||
Organization org,
|
||||
string userEmail,
|
||||
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const bool accessSecretsManager = false;
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendOrganizationConfirmedEmailAsync(org, userEmail, accessSecretsManager);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationConfirmedEmailAsync(org.DisplayName(), userEmail, accessSecretsManager);
|
||||
await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()
|
||||
.DidNotReceive()
|
||||
.SendConfirmationAsync(Arg.Any<Organization>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SendOrganizationConfirmationCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_EnterpriseOrganization_SendsEnterpriseTeamsEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Name = "Test Enterprise Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
|
||||
mail.ToEmails.Contains(userEmail) &&
|
||||
mail.ToEmails.Count() == 1 &&
|
||||
mail.View.OrganizationName == organization.Name &&
|
||||
mail.Subject == GetSubject(organization.Name)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_TeamsOrganization_SendsEnterpriseTeamsEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
organization.Name = "Test Teams Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
|
||||
mail.ToEmails.Contains(userEmail) &&
|
||||
mail.ToEmails.Count() == 1 &&
|
||||
mail.View.OrganizationName == organization.Name &&
|
||||
mail.Subject == GetSubject(organization.Name)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_FamilyOrganization_SendsFamilyFreeEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.FamiliesAnnually;
|
||||
organization.Name = "Test Family Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationFamilyFree>(mail =>
|
||||
mail.ToEmails.Contains(userEmail) &&
|
||||
mail.ToEmails.Count() == 1 &&
|
||||
mail.View.OrganizationName == organization.Name &&
|
||||
mail.Subject == GetSubject(organization.Name)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_FreeOrganization_SendsFamilyFreeEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.Free;
|
||||
organization.Name = "Test Free Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationFamilyFree>(mail =>
|
||||
mail.ToEmails.Contains(userEmail) &&
|
||||
mail.ToEmails.Count() == 1 &&
|
||||
mail.View.OrganizationName == organization.Name &&
|
||||
mail.Subject == GetSubject(organization.Name)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationsAsync_MultipleUsers_SendsSingleEmail(
|
||||
Organization organization,
|
||||
List<string> userEmails,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Name = "Test Enterprise Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationsAsync(organization, userEmails, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
|
||||
mail.ToEmails.SequenceEqual(userEmails) &&
|
||||
mail.View.OrganizationName == organization.Name));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationsAsync_EmptyUserList_DoesNotSendEmail(
|
||||
Organization organization,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Name = "Test Enterprise Org";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationsAsync(organization, [], false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().DidNotReceive()
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationEnterpriseTeams>());
|
||||
await sutProvider.GetDependency<IMailer>().DidNotReceive()
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationFamilyFree>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_HtmlEncodedOrganizationName_DecodesNameCorrectly(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
organization.Name = "Test & Company";
|
||||
var expectedDecodedName = "Test & Company";
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Is<OrganizationConfirmationEnterpriseTeams>(mail =>
|
||||
mail.View.OrganizationName == expectedDecodedName));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_AllEnterpriseTeamsPlanTypes_SendsEnterpriseTeamsEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Test all Enterprise and Teams plan types
|
||||
var enterpriseTeamsPlanTypes = new[]
|
||||
{
|
||||
PlanType.TeamsMonthly2019, PlanType.TeamsAnnually2019,
|
||||
PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020,
|
||||
PlanType.TeamsMonthly2023, PlanType.TeamsAnnually2023,
|
||||
PlanType.TeamsStarter2023, PlanType.TeamsMonthly,
|
||||
PlanType.TeamsAnnually, PlanType.TeamsStarter,
|
||||
PlanType.EnterpriseMonthly2019, PlanType.EnterpriseAnnually2019,
|
||||
PlanType.EnterpriseMonthly2020, PlanType.EnterpriseAnnually2020,
|
||||
PlanType.EnterpriseMonthly2023, PlanType.EnterpriseAnnually2023,
|
||||
PlanType.EnterpriseMonthly, PlanType.EnterpriseAnnually
|
||||
};
|
||||
|
||||
foreach (var planType in enterpriseTeamsPlanTypes)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = planType;
|
||||
organization.Name = "Test Org";
|
||||
sutProvider.GetDependency<IMailer>().ClearReceivedCalls();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationEnterpriseTeams>());
|
||||
await sutProvider.GetDependency<IMailer>().DidNotReceive()
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationFamilyFree>());
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[OrganizationCustomize, BitAutoData]
|
||||
public async Task SendConfirmationAsync_AllFamilyFreePlanTypes_SendsFamilyFreeEmail(
|
||||
Organization organization,
|
||||
string userEmail,
|
||||
SutProvider<SendOrganizationConfirmationCommand> sutProvider)
|
||||
{
|
||||
// Test all Family, Free, and Custom plan types
|
||||
var familyFreePlanTypes = new[]
|
||||
{
|
||||
PlanType.Free, PlanType.FamiliesAnnually2019,
|
||||
PlanType.FamiliesAnnually2025, PlanType.FamiliesAnnually,
|
||||
PlanType.Custom
|
||||
};
|
||||
|
||||
foreach (var planType in familyFreePlanTypes)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = planType;
|
||||
organization.Name = "Test Org";
|
||||
sutProvider.GetDependency<IMailer>().ClearReceivedCalls();
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IMailer>().Received(1)
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationFamilyFree>());
|
||||
await sutProvider.GetDependency<IMailer>().DidNotReceive()
|
||||
.SendEmail(Arg.Any<OrganizationConfirmationEnterpriseTeams>());
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetSubject(string organizationName) => $"You Have Been Confirmed To {organizationName}";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SelfRevokeOrganizationUserCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
public async Task SelfRevokeUser_Success(
|
||||
OrganizationUserType userType,
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser organizationUser,
|
||||
SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.Type = userType;
|
||||
organizationUser.OrganizationId = organizationId;
|
||||
organizationUser.UserId = userId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(organizationId, userId)
|
||||
.Returns(organizationUser);
|
||||
|
||||
// Create policy requirement with confirmed user
|
||||
var policyDetails = new List<PolicyDetails>
|
||||
{
|
||||
new()
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationUserType = userType,
|
||||
PolicyType = PolicyType.OrganizationDataOwnership
|
||||
}
|
||||
};
|
||||
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Enabled,
|
||||
policyDetails);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RevokeAsync(organizationUser.Id);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_SelfRevoked);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncOrgKeysAsync(userId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SelfRevokeUser_WhenUserNotFound_ReturnsNotFoundError(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(organizationId, userId)
|
||||
.Returns((OrganizationUser)null);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OrganizationUserNotFound>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SelfRevokeUser_WhenNotEligible_ReturnsBadRequestError(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser,
|
||||
SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.OrganizationId = organizationId;
|
||||
organizationUser.UserId = userId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(organizationId, userId)
|
||||
.Returns(organizationUser);
|
||||
|
||||
// Policy requirement with no policies (disabled)
|
||||
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Disabled,
|
||||
Enumerable.Empty<PolicyDetails>());
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<NotEligibleForSelfRevoke>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SelfRevokeUser_WhenLastOwner_ReturnsBadRequestError(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
|
||||
SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.OrganizationId = organizationId;
|
||||
organizationUser.UserId = userId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(organizationId, userId)
|
||||
.Returns(organizationUser);
|
||||
|
||||
// Create policy requirement with confirmed owner
|
||||
var policyDetails = new List<PolicyDetails>
|
||||
{
|
||||
new()
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationUserType = OrganizationUserType.Owner,
|
||||
PolicyType = PolicyType.OrganizationDataOwnership
|
||||
}
|
||||
};
|
||||
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Enabled,
|
||||
policyDetails);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>(), true)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<LastOwnerCannotSelfRevoke>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SelfRevokeUser_WhenOwnerButNotLastOwner_Success(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,
|
||||
SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.OrganizationId = organizationId;
|
||||
organizationUser.UserId = userId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(organizationId, userId)
|
||||
.Returns(organizationUser);
|
||||
|
||||
// Create policy requirement with confirmed owner
|
||||
var policyDetails = new List<PolicyDetails>
|
||||
{
|
||||
new()
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
|
||||
OrganizationUserType = OrganizationUserType.Owner,
|
||||
PolicyType = PolicyType.OrganizationDataOwnership
|
||||
}
|
||||
};
|
||||
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Enabled,
|
||||
policyDetails);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>(), true)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RevokeAsync(organizationUser.Id);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_SelfRevoked);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
|
||||
using Bit.Core.Billing.Organizations.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -162,8 +163,9 @@ public class OrganizationUpdateCommandTests
|
||||
OrganizationId = organizationId,
|
||||
Name = organization.Name,
|
||||
BillingEmail = organization.BillingEmail,
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
Keys = new PublicKeyEncryptionKeyPairData(
|
||||
wrappedPrivateKey: encryptedPrivateKey,
|
||||
publicKey: publicKey)
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -207,8 +209,9 @@ public class OrganizationUpdateCommandTests
|
||||
OrganizationId = organizationId,
|
||||
Name = organization.Name,
|
||||
BillingEmail = organization.BillingEmail,
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newEncryptedPrivateKey
|
||||
Keys = new PublicKeyEncryptionKeyPairData(
|
||||
wrappedPrivateKey: newEncryptedPrivateKey,
|
||||
publicKey: newPublicKey)
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -394,8 +397,9 @@ public class OrganizationUpdateCommandTests
|
||||
OrganizationId = organizationId,
|
||||
Name = newName, // Should be ignored
|
||||
BillingEmail = newBillingEmail, // Should be ignored
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
Keys = new PublicKeyEncryptionKeyPairData(
|
||||
wrappedPrivateKey: encryptedPrivateKey,
|
||||
publicKey: publicKey)
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -36,6 +36,73 @@ public class OrganizationDataOwnershipPolicyRequirementFactoryTests
|
||||
Assert.Equal(PolicyType.OrganizationDataOwnership, sutProvider.Sut.PolicyType);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void EligibleForSelfRevoke_WithConfirmedUser_ReturnsTrue(
|
||||
Guid organizationId,
|
||||
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,
|
||||
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
policies[0].OrganizationId = organizationId;
|
||||
var requirement = sutProvider.Sut.Create(policies);
|
||||
|
||||
// Act
|
||||
var result = requirement.EligibleForSelfRevoke(organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void EligibleForSelfRevoke_WithInvitedUser_ReturnsFalse(
|
||||
Guid organizationId,
|
||||
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Invited)] PolicyDetails[] policies,
|
||||
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
policies[0].OrganizationId = organizationId;
|
||||
var requirement = sutProvider.Sut.Create(policies);
|
||||
|
||||
// Act
|
||||
var result = requirement.EligibleForSelfRevoke(organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void EligibleForSelfRevoke_WithNoPolicies_ReturnsFalse(
|
||||
Guid organizationId,
|
||||
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var requirement = sutProvider.Sut.Create([]);
|
||||
|
||||
// Act
|
||||
var result = requirement.EligibleForSelfRevoke(organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void EligibleForSelfRevoke_WithDifferentOrganization_ReturnsFalse(
|
||||
Guid organizationId,
|
||||
Guid differentOrganizationId,
|
||||
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,
|
||||
SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
policies[0].OrganizationId = differentOrganizationId;
|
||||
var requirement = sutProvider.Sut.Create(policies);
|
||||
|
||||
// Act
|
||||
var result = requirement.EligibleForSelfRevoke(organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void GetDefaultCollectionRequestOnPolicyEnable_WithConfirmedUser_ReturnsTrue(
|
||||
[PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,
|
||||
|
||||
@@ -6,7 +6,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -20,29 +19,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
{
|
||||
private const string _defaultUserCollectionName = "Default";
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ExecuteSideEffectsAsync_FeatureFlagDisabled_DoesNothing(
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
|
||||
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(false);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ExecuteSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing(
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
|
||||
@@ -54,10 +30,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
@@ -80,10 +52,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
@@ -234,10 +202,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
policyUpdate.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, metadata);
|
||||
|
||||
// Act
|
||||
@@ -264,39 +228,10 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
IPolicyRepository policyRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService);
|
||||
var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory]);
|
||||
return sut;
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ExecutePostUpsertSideEffectAsync_FeatureFlagDisabled_DoesNothing(
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,
|
||||
[Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,
|
||||
SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(false);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.UpsertDefaultCollectionsAsync(default, default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ExecutePostUpsertSideEffectAsync_PolicyAlreadyEnabled_DoesNothing(
|
||||
[PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,
|
||||
@@ -308,10 +243,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
@@ -334,10 +265,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
|
||||
|
||||
// Act
|
||||
@@ -432,10 +359,6 @@ public class OrganizationDataOwnershipPolicyValidatorTests
|
||||
previousPolicyState.OrganizationId = policyUpdate.OrganizationId;
|
||||
policyUpdate.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequest = new SavePolicyModel(policyUpdate, metadata);
|
||||
|
||||
// Act
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
public class IntegrationHandlerTests
|
||||
{
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage()
|
||||
{
|
||||
var sut = new TestIntegrationHandler();
|
||||
var expected = new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
|
||||
MessageId = "TestMessageId",
|
||||
OrganizationId = "TestOrganizationId",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RenderedTemplate = "Template",
|
||||
DelayUntilDate = null,
|
||||
RetryCount = 0
|
||||
};
|
||||
|
||||
var result = await sut.HandleAsync(expected.ToJson());
|
||||
var typedResult = Assert.IsType<IntegrationMessage<WebhookIntegrationConfigurationDetails>>(result.Message);
|
||||
|
||||
Assert.Equal(expected.MessageId, typedResult.MessageId);
|
||||
Assert.Equal(expected.OrganizationId, typedResult.OrganizationId);
|
||||
Assert.Equal(expected.Configuration, typedResult.Configuration);
|
||||
Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate);
|
||||
Assert.Equal(expected.IntegrationType, typedResult.IntegrationType);
|
||||
}
|
||||
|
||||
private class TestIntegrationHandler : IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
public override Task<IntegrationHandlerResult> HandleAsync(
|
||||
IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var result = new IntegrationHandlerResult(success: true, message: message);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
@@ -214,6 +214,7 @@ If you believe you need to change the version for a valid reason, please discuss
|
||||
AllowAdminAccessToAllCollectionItems = true,
|
||||
UseOrganizationDomains = true,
|
||||
UseAdminSponsoredFamilies = false,
|
||||
UseDisableSmAdsForUsers = false,
|
||||
UsePhishingBlocker = false,
|
||||
};
|
||||
}
|
||||
@@ -260,4 +261,34 @@ If you believe you need to change the version for a valid reason, please discuss
|
||||
.Returns([0x00, 0x01, 0x02, 0x03]); // Dummy signature for hash testing
|
||||
return mockService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that UseDisableSmAdsForUsers claim is properly generated in the license Token
|
||||
/// and that VerifyData correctly validates the claim.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[BitAutoData(true)]
|
||||
[BitAutoData(false)]
|
||||
public void OrganizationLicense_UseDisableSmAdsForUsers_ClaimGenerationAndValidation(bool useDisableSmAdsForUsers, ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
// Arrange
|
||||
var organization = CreateDeterministicOrganization();
|
||||
organization.UseDisableSmAdsForUsers = useDisableSmAdsForUsers;
|
||||
|
||||
var subscriptionInfo = CreateDeterministicSubscriptionInfo();
|
||||
var installationId = new Guid("78900000-0000-0000-0000-000000000123");
|
||||
var mockLicensingService = CreateMockLicensingService();
|
||||
|
||||
var license = new OrganizationLicense(organization, subscriptionInfo, installationId, mockLicensingService);
|
||||
license.Expires = DateTime.MaxValue; // Prevent expiration during test
|
||||
|
||||
var globalSettings = Substitute.For<IGlobalSettings>();
|
||||
globalSettings.Installation.Returns(new GlobalSettings.InstallationSettings
|
||||
{
|
||||
Id = installationId
|
||||
});
|
||||
|
||||
// Act & Assert - Verify VerifyData passes with the UseDisableSmAdsForUsers value
|
||||
Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class UpdateOrganizationLicenseCommandTests
|
||||
"Hash", "Signature", "SignatureBytes", "InstallationId", "Expires",
|
||||
"ExpirationWithoutGracePeriod", "Token", "LimitCollectionCreationDeletion",
|
||||
"LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems",
|
||||
"UseOrganizationDomains", "UseAdminSponsoredFamilies", "UseAutomaticUserConfirmation", "UsePhishingBlocker") &&
|
||||
"UseOrganizationDomains", "UseAdminSponsoredFamilies", "UseAutomaticUserConfirmation", "UsePhishingBlocker", "UseDisableSmAdsForUsers") &&
|
||||
// Same property but different name, use explicit mapping
|
||||
org.ExpirationDate == license.Expires));
|
||||
}
|
||||
|
||||
@@ -407,4 +407,85 @@ public class UpdateBillingAddressCommandTests
|
||||
options => options.Type == TaxIdType.SpanishNIF &&
|
||||
options.Value == input.TaxId.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_BusinessOrganization_UpdatingWithSameTaxId_DeletesBeforeCreating()
|
||||
{
|
||||
var organization = new Organization
|
||||
{
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
GatewayCustomerId = "cus_123",
|
||||
GatewaySubscriptionId = "sub_123"
|
||||
};
|
||||
|
||||
var input = new BillingAddress
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345",
|
||||
Line1 = "123 Main St.",
|
||||
Line2 = "Suite 100",
|
||||
City = "New York",
|
||||
State = "NY",
|
||||
TaxId = new TaxID("us_ein", "987654321")
|
||||
};
|
||||
|
||||
var existingTaxId = new TaxId { Id = "tax_id_123", Type = "us_ein", Value = "987654321" };
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345",
|
||||
Line1 = "123 Main St.",
|
||||
Line2 = "Suite 100",
|
||||
City = "New York",
|
||||
State = "NY"
|
||||
},
|
||||
Id = organization.GatewayCustomerId,
|
||||
Subscriptions = new StripeList<Subscription>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new Subscription
|
||||
{
|
||||
Id = organization.GatewaySubscriptionId,
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }
|
||||
}
|
||||
]
|
||||
},
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [existingTaxId]
|
||||
}
|
||||
};
|
||||
|
||||
_stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Address.Matches(input) &&
|
||||
options.HasExpansions("subscriptions", "tax_ids") &&
|
||||
options.TaxExempt == TaxExempt.None
|
||||
)).Returns(customer);
|
||||
|
||||
var newTaxId = new TaxId { Id = "tax_id_456", Type = "us_ein", Value = "987654321" };
|
||||
_stripeAdapter.CreateTaxIdAsync(customer.Id, Arg.Is<TaxIdCreateOptions>(
|
||||
options => options.Type == "us_ein" && options.Value == "987654321"
|
||||
)).Returns(newTaxId);
|
||||
|
||||
var result = await _command.Run(organization, input);
|
||||
|
||||
Assert.True(result.IsT0);
|
||||
var output = result.AsT0;
|
||||
Assert.Equivalent(input, output);
|
||||
|
||||
// Verify that deletion happens before creation
|
||||
Received.InOrder(() =>
|
||||
{
|
||||
_stripeAdapter.DeleteTaxIdAsync(customer.Id, existingTaxId.Id);
|
||||
_stripeAdapter.CreateTaxIdAsync(customer.Id, Arg.Any<TaxIdCreateOptions>());
|
||||
});
|
||||
|
||||
await _stripeAdapter.Received(1).DeleteTaxIdAsync(customer.Id, existingTaxId.Id);
|
||||
await _stripeAdapter.Received(1).CreateTaxIdAsync(customer.Id, Arg.Is<TaxIdCreateOptions>(
|
||||
options => options.Type == "us_ein" && options.Value == "987654321"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
|
||||
using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Premium.Commands;
|
||||
|
||||
public class UpdatePremiumStorageCommandTests
|
||||
{
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly IUserService _userService = Substitute.For<IUserService>();
|
||||
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||
private readonly PremiumPlan _premiumPlan;
|
||||
private readonly UpdatePremiumStorageCommand _command;
|
||||
|
||||
public UpdatePremiumStorageCommandTests()
|
||||
{
|
||||
// Setup default premium plan with standard pricing
|
||||
_premiumPlan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = "price_premium", Provided = 1 },
|
||||
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = "price_storage", Provided = 1 }
|
||||
};
|
||||
_pricingClient.ListPremiumPlans().Returns(new List<PremiumPlan> { _premiumPlan });
|
||||
|
||||
_command = new UpdatePremiumStorageCommand(
|
||||
_stripeAdapter,
|
||||
_userService,
|
||||
_pricingClient,
|
||||
Substitute.For<ILogger<UpdatePremiumStorageCommand>>());
|
||||
}
|
||||
|
||||
private Subscription CreateMockSubscription(string subscriptionId, int? storageQuantity = null)
|
||||
{
|
||||
var items = new List<SubscriptionItem>();
|
||||
|
||||
// Always add the seat item
|
||||
items.Add(new SubscriptionItem
|
||||
{
|
||||
Id = "si_seat",
|
||||
Price = new Price { Id = "price_premium" },
|
||||
Quantity = 1
|
||||
});
|
||||
|
||||
// Add storage item if quantity is provided
|
||||
if (storageQuantity.HasValue && storageQuantity.Value > 0)
|
||||
{
|
||||
items.Add(new SubscriptionItem
|
||||
{
|
||||
Id = "si_storage",
|
||||
Price = new Price { Id = "price_storage" },
|
||||
Quantity = storageQuantity.Value
|
||||
});
|
||||
}
|
||||
|
||||
return new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = items
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_UserNotPremium_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 5);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("User does not have a premium subscription.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NegativeStorage_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 5;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 4);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, -5);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("Additional storage cannot be negative.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_StorageExceedsMaximum_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 5;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 4);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 100);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("Maximum storage is 100 GB.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NoMaxStorageGb_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = null;
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 5);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Equal("No access to storage.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_StorageExceedsCurrentUsage_ReturnsBadRequest(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 10;
|
||||
user.Storage = 5L * 1024 * 1024 * 1024; // 5 GB currently used
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 9);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT1);
|
||||
var badRequest = result.AsT1;
|
||||
Assert.Contains("You are currently using", badRequest.Response);
|
||||
Assert.Contains("Delete some stored data first", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_SameStorageAmount_Idempotent(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 5;
|
||||
user.Storage = 2L * 1024 * 1024 * 1024;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 4);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 4);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
// Verify subscription was fetched but NOT updated
|
||||
await _stripeAdapter.Received(1).GetSubscriptionAsync("sub_123");
|
||||
await _stripeAdapter.DidNotReceive().UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_IncreaseStorage_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 5;
|
||||
user.Storage = 2L * 1024 * 1024 * 1024;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 4);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 9);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
// Verify subscription was updated
|
||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||
opts.Items.Count == 1 &&
|
||||
opts.Items[0].Id == "si_storage" &&
|
||||
opts.Items[0].Quantity == 9 &&
|
||||
opts.ProrationBehavior == "create_prorations"));
|
||||
|
||||
// Verify user was saved
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>
|
||||
u.Id == user.Id &&
|
||||
u.MaxStorageGb == 10));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_AddStorageFromZero_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 1;
|
||||
user.Storage = 500L * 1024 * 1024;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", null);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 9);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
// Verify subscription was updated with new storage item
|
||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||
opts.Items.Count == 1 &&
|
||||
opts.Items[0].Price == "price_storage" &&
|
||||
opts.Items[0].Quantity == 9));
|
||||
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 10));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_DecreaseStorage_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 10;
|
||||
user.Storage = 2L * 1024 * 1024 * 1024;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 9);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 2);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
// Verify subscription was updated
|
||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||
opts.Items.Count == 1 &&
|
||||
opts.Items[0].Id == "si_storage" &&
|
||||
opts.Items[0].Quantity == 2));
|
||||
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 3));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_RemoveAllAdditionalStorage_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 10;
|
||||
user.Storage = 500L * 1024 * 1024;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 9);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
// Verify subscription item was deleted
|
||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||
opts.Items.Count == 1 &&
|
||||
opts.Items[0].Id == "si_storage" &&
|
||||
opts.Items[0].Deleted == true));
|
||||
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 1));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_MaximumStorage_Success(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 5;
|
||||
user.Storage = 2L * 1024 * 1024 * 1024;
|
||||
user.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var subscription = CreateMockSubscription("sub_123", 4);
|
||||
_stripeAdapter.GetSubscriptionAsync("sub_123").Returns(subscription);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, 99);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(
|
||||
"sub_123",
|
||||
Arg.Is<SubscriptionUpdateOptions>(opts =>
|
||||
opts.Items[0].Quantity == 99));
|
||||
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 100));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Subscriptions.Commands;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -17,20 +20,19 @@ using static StripeConstants;
|
||||
public class RestartSubscriptionCommandTests
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
private readonly IProviderRepository _providerRepository = Substitute.For<IProviderRepository>();
|
||||
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
|
||||
private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>();
|
||||
private readonly RestartSubscriptionCommand _command;
|
||||
|
||||
public RestartSubscriptionCommandTests()
|
||||
{
|
||||
_command = new RestartSubscriptionCommand(
|
||||
Substitute.For<Microsoft.Extensions.Logging.ILogger<RestartSubscriptionCommand>>(),
|
||||
_organizationRepository,
|
||||
_providerRepository,
|
||||
_pricingClient,
|
||||
_stripeAdapter,
|
||||
_subscriberService,
|
||||
_userRepository);
|
||||
_subscriberService);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -63,11 +65,56 @@ public class RestartSubscriptionCommandTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Organization_Success_ReturnsNone()
|
||||
public async Task Run_Provider_ReturnsUnhandledWithNotSupportedException()
|
||||
{
|
||||
var provider = new Provider { Id = Guid.NewGuid() };
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_123"
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(provider).Returns(existingSubscription);
|
||||
|
||||
var result = await _command.Run(provider);
|
||||
|
||||
Assert.True(result.IsT3);
|
||||
var unhandled = result.AsT3;
|
||||
Assert.IsType<NotSupportedException>(unhandled.Exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_User_ReturnsUnhandledWithNotSupportedException()
|
||||
{
|
||||
var user = new User { Id = Guid.NewGuid() };
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_123"
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(user).Returns(existingSubscription);
|
||||
|
||||
var result = await _command.Run(user);
|
||||
|
||||
Assert.True(result.IsT3);
|
||||
var unhandled = result.AsT3;
|
||||
Assert.IsType<NotSupportedException>(unhandled.Exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Organization_MissingPasswordManagerItem_ReturnsUnhandledWithConflictException()
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var organization = new Organization { Id = organizationId };
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.EnterpriseAnnually
|
||||
};
|
||||
|
||||
var plan = MockPlans.Get(PlanType.EnterpriseAnnually);
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
@@ -77,11 +124,122 @@ public class RestartSubscriptionCommandTests
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 },
|
||||
new SubscriptionItem { Price = new Price { Id = "price_2" }, Quantity = 2 }
|
||||
new SubscriptionItem { Price = new Price { Id = "some-other-price-id" }, Quantity = 10 }
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string> { ["key"] = "value" }
|
||||
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
|
||||
_pricingClient.ListPlans().Returns([plan]);
|
||||
|
||||
var result = await _command.Run(organization);
|
||||
|
||||
Assert.True(result.IsT3);
|
||||
var unhandled = result.AsT3;
|
||||
Assert.IsType<ConflictException>(unhandled.Exception);
|
||||
Assert.Equal("Organization's subscription does not have a Password Manager subscription item.", unhandled.Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Organization_PlanNotFound_ReturnsUnhandledWithConflictException()
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.EnterpriseAnnually
|
||||
};
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { Price = new Price { Id = "some-price-id" }, Quantity = 10 }
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
|
||||
// Return a plan list that doesn't contain the organization's plan type
|
||||
_pricingClient.ListPlans().Returns([MockPlans.Get(PlanType.TeamsAnnually)]);
|
||||
|
||||
var result = await _command.Run(organization);
|
||||
|
||||
Assert.True(result.IsT3);
|
||||
var unhandled = result.AsT3;
|
||||
Assert.IsType<ConflictException>(unhandled.Exception);
|
||||
Assert.Equal("Could not find plan for organization's plan type", unhandled.Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Organization_DisabledPlanWithNoEnabledReplacement_ReturnsUnhandledWithConflictException()
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.EnterpriseAnnually2023
|
||||
};
|
||||
|
||||
var oldPlan = new DisabledEnterprisePlan2023(true);
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_old",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeSeatPlanId }, Quantity = 20 }
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
|
||||
// Return only the disabled plan, with no enabled replacement
|
||||
_pricingClient.ListPlans().Returns([oldPlan]);
|
||||
|
||||
var result = await _command.Run(organization);
|
||||
|
||||
Assert.True(result.IsT3);
|
||||
var unhandled = result.AsT3;
|
||||
Assert.IsType<ConflictException>(unhandled.Exception);
|
||||
Assert.Equal("Could not find the current, enabled plan for organization's tier and cadence", unhandled.Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Organization_WithNonDisabledPlan_PasswordManagerOnly_Success()
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.EnterpriseAnnually
|
||||
};
|
||||
|
||||
var plan = MockPlans.Get(PlanType.EnterpriseAnnually);
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 10 }
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
|
||||
};
|
||||
|
||||
var newSubscription = new Subscription
|
||||
@@ -89,30 +247,26 @@ public class RestartSubscriptionCommandTests
|
||||
Id = "sub_new",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
|
||||
]
|
||||
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
|
||||
}
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
|
||||
_pricingClient.ListPlans().Returns([plan]);
|
||||
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
|
||||
|
||||
var result = await _command.Run(organization);
|
||||
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is((SubscriptionCreateOptions options) =>
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true &&
|
||||
options.CollectionMethod == CollectionMethod.ChargeAutomatically &&
|
||||
options.Customer == "cus_123" &&
|
||||
options.Items.Count == 2 &&
|
||||
options.Items[0].Price == "price_1" &&
|
||||
options.Items[0].Quantity == 1 &&
|
||||
options.Items[1].Price == "price_2" &&
|
||||
options.Items[1].Quantity == 2 &&
|
||||
options.Metadata["key"] == "value" &&
|
||||
options.Items.Count == 1 &&
|
||||
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
|
||||
options.Items[0].Quantity == 10 &&
|
||||
options.Metadata["organizationId"] == organizationId.ToString() &&
|
||||
options.OffSession == true &&
|
||||
options.TrialPeriodDays == 0));
|
||||
|
||||
@@ -120,96 +274,417 @@ public class RestartSubscriptionCommandTests
|
||||
org.Id == organizationId &&
|
||||
org.GatewaySubscriptionId == "sub_new" &&
|
||||
org.Enabled == true &&
|
||||
org.ExpirationDate == currentPeriodEnd));
|
||||
org.ExpirationDate == currentPeriodEnd &&
|
||||
org.PlanType == PlanType.EnterpriseAnnually));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Provider_Success_ReturnsNone()
|
||||
public async Task Run_Organization_WithNonDisabledPlan_WithStorage_Success()
|
||||
{
|
||||
var providerId = Guid.NewGuid();
|
||||
var provider = new Provider { Id = providerId };
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = [new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }]
|
||||
},
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var newSubscription = new Subscription
|
||||
{
|
||||
Id = "sub_new",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(provider).Returns(existingSubscription);
|
||||
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
|
||||
|
||||
var result = await _command.Run(provider);
|
||||
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
|
||||
await _providerRepository.Received(1).ReplaceAsync(Arg.Is<Provider>(prov =>
|
||||
prov.Id == providerId &&
|
||||
prov.GatewaySubscriptionId == "sub_new" &&
|
||||
prov.Enabled == true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_User_Success_ReturnsNone()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new User { Id = userId };
|
||||
var organizationId = Guid.NewGuid();
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.TeamsAnnually
|
||||
};
|
||||
|
||||
var plan = MockPlans.Get(PlanType.TeamsAnnually);
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = [new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }]
|
||||
},
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var newSubscription = new Subscription
|
||||
{
|
||||
Id = "sub_new",
|
||||
CustomerId = "cus_456",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
|
||||
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 5 },
|
||||
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 3 }
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
|
||||
};
|
||||
|
||||
var newSubscription = new Subscription
|
||||
{
|
||||
Id = "sub_new_2",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
|
||||
}
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(user).Returns(existingSubscription);
|
||||
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
|
||||
_pricingClient.ListPlans().Returns([plan]);
|
||||
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
|
||||
|
||||
var result = await _command.Run(user);
|
||||
var result = await _command.Run(organization);
|
||||
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
options.Items.Count == 2 &&
|
||||
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
|
||||
options.Items[0].Quantity == 5 &&
|
||||
options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&
|
||||
options.Items[1].Quantity == 3));
|
||||
|
||||
await _userRepository.Received(1).ReplaceAsync(Arg.Is<User>(u =>
|
||||
u.Id == userId &&
|
||||
u.GatewaySubscriptionId == "sub_new" &&
|
||||
u.Premium == true &&
|
||||
u.PremiumExpirationDate == currentPeriodEnd));
|
||||
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
|
||||
org.Id == organizationId &&
|
||||
org.GatewaySubscriptionId == "sub_new_2" &&
|
||||
org.Enabled == true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Organization_WithSecretsManager_Success()
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.EnterpriseMonthly
|
||||
};
|
||||
|
||||
var plan = MockPlans.Get(PlanType.EnterpriseMonthly);
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_789",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 15 },
|
||||
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 2 },
|
||||
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 10 },
|
||||
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeServiceAccountPlanId }, Quantity = 100 }
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
|
||||
};
|
||||
|
||||
var newSubscription = new Subscription
|
||||
{
|
||||
Id = "sub_new_3",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
|
||||
}
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
|
||||
_pricingClient.ListPlans().Returns([plan]);
|
||||
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
|
||||
|
||||
var result = await _command.Run(organization);
|
||||
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
options.Items.Count == 4 &&
|
||||
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
|
||||
options.Items[0].Quantity == 15 &&
|
||||
options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&
|
||||
options.Items[1].Quantity == 2 &&
|
||||
options.Items[2].Price == plan.SecretsManager.StripeSeatPlanId &&
|
||||
options.Items[2].Quantity == 10 &&
|
||||
options.Items[3].Price == plan.SecretsManager.StripeServiceAccountPlanId &&
|
||||
options.Items[3].Quantity == 100));
|
||||
|
||||
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
|
||||
org.Id == organizationId &&
|
||||
org.GatewaySubscriptionId == "sub_new_3" &&
|
||||
org.Enabled == true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Organization_WithDisabledPlan_UpgradesToNewPlan_Success()
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.EnterpriseAnnually2023
|
||||
};
|
||||
|
||||
var oldPlan = new DisabledEnterprisePlan2023(true);
|
||||
var newPlan = MockPlans.Get(PlanType.EnterpriseAnnually);
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_old",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeSeatPlanId }, Quantity = 20 },
|
||||
new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeStoragePlanId }, Quantity = 5 }
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
|
||||
};
|
||||
|
||||
var newSubscription = new Subscription
|
||||
{
|
||||
Id = "sub_upgraded",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
|
||||
}
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
|
||||
_pricingClient.ListPlans().Returns([oldPlan, newPlan]);
|
||||
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
|
||||
|
||||
var result = await _command.Run(organization);
|
||||
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
options.Items.Count == 2 &&
|
||||
options.Items[0].Price == newPlan.PasswordManager.StripeSeatPlanId &&
|
||||
options.Items[0].Quantity == 20 &&
|
||||
options.Items[1].Price == newPlan.PasswordManager.StripeStoragePlanId &&
|
||||
options.Items[1].Quantity == 5));
|
||||
|
||||
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
|
||||
org.Id == organizationId &&
|
||||
org.GatewaySubscriptionId == "sub_upgraded" &&
|
||||
org.Enabled == true &&
|
||||
org.PlanType == PlanType.EnterpriseAnnually &&
|
||||
org.Plan == newPlan.Name &&
|
||||
org.SelfHost == newPlan.HasSelfHost &&
|
||||
org.UsePolicies == newPlan.HasPolicies &&
|
||||
org.UseGroups == newPlan.HasGroups &&
|
||||
org.UseDirectory == newPlan.HasDirectory &&
|
||||
org.UseEvents == newPlan.HasEvents &&
|
||||
org.UseTotp == newPlan.HasTotp &&
|
||||
org.Use2fa == newPlan.Has2fa &&
|
||||
org.UseApi == newPlan.HasApi &&
|
||||
org.UseSso == newPlan.HasSso &&
|
||||
org.UseOrganizationDomains == newPlan.HasOrganizationDomains &&
|
||||
org.UseKeyConnector == newPlan.HasKeyConnector &&
|
||||
org.UseScim == newPlan.HasScim &&
|
||||
org.UseResetPassword == newPlan.HasResetPassword &&
|
||||
org.UsersGetPremium == newPlan.UsersGetPremium &&
|
||||
org.UseCustomPermissions == newPlan.HasCustomPermissions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Organization_WithStorageAndSecretManagerButNoServiceAccounts_Success()
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.TeamsAnnually
|
||||
};
|
||||
|
||||
var plan = MockPlans.Get(PlanType.TeamsAnnually);
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_complex",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 12 },
|
||||
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 8 },
|
||||
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 6 }
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
|
||||
};
|
||||
|
||||
var newSubscription = new Subscription
|
||||
{
|
||||
Id = "sub_complex",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
|
||||
}
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
|
||||
_pricingClient.ListPlans().Returns([plan]);
|
||||
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
|
||||
|
||||
var result = await _command.Run(organization);
|
||||
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
options.Items.Count == 3 &&
|
||||
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
|
||||
options.Items[0].Quantity == 12 &&
|
||||
options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&
|
||||
options.Items[1].Quantity == 8 &&
|
||||
options.Items[2].Price == plan.SecretsManager.StripeSeatPlanId &&
|
||||
options.Items[2].Quantity == 6));
|
||||
|
||||
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
|
||||
org.Id == organizationId &&
|
||||
org.GatewaySubscriptionId == "sub_complex" &&
|
||||
org.Enabled == true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_Organization_WithSecretsManagerOnly_NoServiceAccounts_Success()
|
||||
{
|
||||
var organizationId = Guid.NewGuid();
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.TeamsMonthly
|
||||
};
|
||||
|
||||
var plan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
var existingSubscription = new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
CustomerId = "cus_sm",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 8 },
|
||||
new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 5 }
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string> { ["organizationId"] = organizationId.ToString() }
|
||||
};
|
||||
|
||||
var newSubscription = new Subscription
|
||||
{
|
||||
Id = "sub_sm",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]
|
||||
}
|
||||
};
|
||||
|
||||
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
|
||||
_pricingClient.ListPlans().Returns([plan]);
|
||||
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
|
||||
|
||||
var result = await _command.Run(organization);
|
||||
|
||||
Assert.True(result.IsT0);
|
||||
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
options.Items.Count == 2 &&
|
||||
options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
|
||||
options.Items[0].Quantity == 8 &&
|
||||
options.Items[1].Price == plan.SecretsManager.StripeSeatPlanId &&
|
||||
options.Items[1].Quantity == 5));
|
||||
|
||||
await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>
|
||||
org.Id == organizationId &&
|
||||
org.GatewaySubscriptionId == "sub_sm" &&
|
||||
org.Enabled == true));
|
||||
}
|
||||
|
||||
private record DisabledEnterprisePlan2023 : Bit.Core.Models.StaticStore.Plan
|
||||
{
|
||||
public DisabledEnterprisePlan2023(bool isAnnual)
|
||||
{
|
||||
Type = PlanType.EnterpriseAnnually2023;
|
||||
ProductTier = ProductTierType.Enterprise;
|
||||
Name = "Enterprise (Annually) 2023";
|
||||
IsAnnual = isAnnual;
|
||||
NameLocalizationKey = "planNameEnterprise";
|
||||
DescriptionLocalizationKey = "planDescEnterprise";
|
||||
CanBeUsedByBusiness = true;
|
||||
TrialPeriodDays = 7;
|
||||
HasPolicies = true;
|
||||
HasSelfHost = true;
|
||||
HasGroups = true;
|
||||
HasDirectory = true;
|
||||
HasEvents = true;
|
||||
HasTotp = true;
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
HasSso = true;
|
||||
HasOrganizationDomains = true;
|
||||
HasKeyConnector = true;
|
||||
HasScim = true;
|
||||
HasResetPassword = true;
|
||||
UsersGetPremium = true;
|
||||
HasCustomPermissions = true;
|
||||
UpgradeSortOrder = 4;
|
||||
DisplaySortOrder = 4;
|
||||
LegacyYear = 2024;
|
||||
Disabled = true;
|
||||
|
||||
PasswordManager = new PasswordManagerFeatures(isAnnual);
|
||||
SecretsManager = new SecretsManagerFeatures(isAnnual);
|
||||
}
|
||||
|
||||
private record SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||
{
|
||||
public SecretsManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BasePrice = 0;
|
||||
BaseServiceAccount = 200;
|
||||
HasAdditionalSeatsOption = true;
|
||||
HasAdditionalServiceAccountOption = true;
|
||||
AllowSeatAutoscale = true;
|
||||
AllowServiceAccountsAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually-2023";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2023-annually";
|
||||
SeatPrice = 144;
|
||||
AdditionalPricePerServiceAccount = 12;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly-2023";
|
||||
StripeServiceAccountPlanId = "secrets-manager-service-account-2023-monthly";
|
||||
SeatPrice = 13;
|
||||
AdditionalPricePerServiceAccount = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||
{
|
||||
public PasswordManagerFeatures(bool isAnnual)
|
||||
{
|
||||
BaseSeats = 0;
|
||||
BaseStorageGb = 1;
|
||||
HasAdditionalStorageOption = true;
|
||||
HasAdditionalSeatsOption = true;
|
||||
AllowSeatAutoscale = true;
|
||||
|
||||
if (isAnnual)
|
||||
{
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-enterprise-org-seat-annually-old";
|
||||
SeatPrice = 72;
|
||||
}
|
||||
else
|
||||
{
|
||||
StripeSeatPlanId = "2023-enterprise-seat-monthly-old";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
SeatPrice = 7;
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Core.Dirt.Services.NoopImplementations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Bot.Builder.Integration.AspNet.Core;
|
||||
@@ -19,7 +22,7 @@ using StackExchange.Redis;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.EventIntegrations;
|
||||
namespace Bit.Core.Test.Dirt.EventIntegrations;
|
||||
|
||||
public class EventIntegrationServiceCollectionExtensionsTests
|
||||
{
|
||||
@@ -200,7 +203,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
|
||||
});
|
||||
|
||||
Assert.True(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
|
||||
@@ -214,7 +218,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
["GlobalSettings:EventLogging:RabbitMq:HostName"] = null,
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
|
||||
});
|
||||
|
||||
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
|
||||
@@ -228,7 +233,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = null,
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
|
||||
});
|
||||
|
||||
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
|
||||
@@ -242,21 +248,38 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = null,
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
|
||||
});
|
||||
|
||||
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRabbitMqEnabled_MissingExchangeName_ReturnsFalse()
|
||||
public void IsRabbitMqEnabled_MissingEventExchangeName_ReturnsFalse()
|
||||
{
|
||||
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
{
|
||||
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = null
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = null,
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
|
||||
});
|
||||
|
||||
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRabbitMqEnabled_MissingIntegrationExchangeName_ReturnsFalse()
|
||||
{
|
||||
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
{
|
||||
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = null
|
||||
});
|
||||
|
||||
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
|
||||
@@ -268,7 +291,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
{
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
|
||||
});
|
||||
|
||||
Assert.True(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
|
||||
@@ -280,19 +304,34 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
{
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = null,
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
|
||||
});
|
||||
|
||||
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsAzureServiceBusEnabled_MissingTopicName_ReturnsFalse()
|
||||
public void IsAzureServiceBusEnabled_MissingEventTopicName_ReturnsFalse()
|
||||
{
|
||||
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
{
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = null
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = null,
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
|
||||
});
|
||||
|
||||
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsAzureServiceBusEnabled_MissingIntegrationTopicName_ReturnsFalse()
|
||||
{
|
||||
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
{
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = null
|
||||
});
|
||||
|
||||
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
|
||||
@@ -601,7 +640,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
|
||||
});
|
||||
|
||||
// Add prerequisites
|
||||
@@ -624,7 +664,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
{
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
|
||||
});
|
||||
|
||||
// Add prerequisites
|
||||
@@ -650,8 +691,10 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
|
||||
});
|
||||
|
||||
// Add prerequisites
|
||||
@@ -694,7 +737,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
{
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
|
||||
});
|
||||
|
||||
services.AddEventWriteServices(globalSettings);
|
||||
@@ -712,7 +756,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
|
||||
});
|
||||
|
||||
services.AddEventWriteServices(globalSettings);
|
||||
@@ -769,10 +814,12 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
{
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration",
|
||||
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
|
||||
});
|
||||
|
||||
services.AddEventWriteServices(globalSettings);
|
||||
@@ -789,7 +836,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
|
||||
{
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
|
||||
["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration"
|
||||
});
|
||||
|
||||
// Add prerequisites
|
||||
@@ -826,7 +874,8 @@ public class EventIntegrationServiceCollectionExtensionsTests
|
||||
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
|
||||
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
|
||||
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
|
||||
["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration"
|
||||
});
|
||||
|
||||
// Add prerequisites
|
||||
@@ -1,9 +1,10 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -11,7 +12,7 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class CreateOrganizationIntegrationConfigurationCommandTests
|
||||
@@ -1,8 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -10,7 +11,7 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DeleteOrganizationIntegrationConfigurationCommandTests
|
||||
@@ -1,13 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetOrganizationIntegrationConfigurationsQueryTests
|
||||
@@ -1,9 +1,10 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -11,7 +12,7 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class UpdateOrganizationIntegrationConfigurationCommandTests
|
||||
@@ -1,8 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -10,7 +10,7 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class CreateOrganizationIntegrationCommandTests
|
||||
@@ -1,8 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -10,7 +10,7 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DeleteOrganizationIntegrationCommandTests
|
||||
@@ -1,12 +1,12 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetOrganizationIntegrationsQueryTests
|
||||
@@ -1,8 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -10,7 +10,7 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||
namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class UpdateOrganizationIntegrationCommandTests
|
||||
@@ -0,0 +1,128 @@
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
|
||||
public class IntegrationHandlerResultTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void Succeed_SetsSuccessTrue_CategoryNull(IntegrationMessage message)
|
||||
{
|
||||
var result = IntegrationHandlerResult.Succeed(message);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Null(result.Category);
|
||||
Assert.Equal(message, result.Message);
|
||||
Assert.Null(result.FailureReason);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Fail_WithCategory_SetsSuccessFalse_CategorySet(IntegrationMessage message)
|
||||
{
|
||||
var category = IntegrationFailureCategory.AuthenticationFailed;
|
||||
var failureReason = "Invalid credentials";
|
||||
|
||||
var result = IntegrationHandlerResult.Fail(message, category, failureReason);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(category, result.Category);
|
||||
Assert.Equal(failureReason, result.FailureReason);
|
||||
Assert.Equal(message, result.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Fail_WithDelayUntil_SetsDelayUntilDate(IntegrationMessage message)
|
||||
{
|
||||
var delayUntil = DateTime.UtcNow.AddMinutes(5);
|
||||
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message,
|
||||
IntegrationFailureCategory.RateLimited,
|
||||
"Rate limited",
|
||||
delayUntil
|
||||
);
|
||||
|
||||
Assert.Equal(delayUntil, result.DelayUntilDate);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Retryable_RateLimited_ReturnsTrue(IntegrationMessage message)
|
||||
{
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message,
|
||||
IntegrationFailureCategory.RateLimited,
|
||||
"Rate limited"
|
||||
);
|
||||
|
||||
Assert.True(result.Retryable);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Retryable_TransientError_ReturnsTrue(IntegrationMessage message)
|
||||
{
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message,
|
||||
IntegrationFailureCategory.TransientError,
|
||||
"Temporary network issue"
|
||||
);
|
||||
|
||||
Assert.True(result.Retryable);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Retryable_AuthenticationFailed_ReturnsFalse(IntegrationMessage message)
|
||||
{
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message,
|
||||
IntegrationFailureCategory.AuthenticationFailed,
|
||||
"Invalid token"
|
||||
);
|
||||
|
||||
Assert.False(result.Retryable);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Retryable_ConfigurationError_ReturnsFalse(IntegrationMessage message)
|
||||
{
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message,
|
||||
IntegrationFailureCategory.ConfigurationError,
|
||||
"Channel not found"
|
||||
);
|
||||
|
||||
Assert.False(result.Retryable);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Retryable_ServiceUnavailable_ReturnsTrue(IntegrationMessage message)
|
||||
{
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message,
|
||||
IntegrationFailureCategory.ServiceUnavailable,
|
||||
"Service is down"
|
||||
);
|
||||
|
||||
Assert.True(result.Retryable);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Retryable_PermanentFailure_ReturnsFalse(IntegrationMessage message)
|
||||
{
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message,
|
||||
IntegrationFailureCategory.PermanentFailure,
|
||||
"Permanent failure"
|
||||
);
|
||||
|
||||
Assert.False(result.Retryable);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Retryable_SuccessCase_ReturnsFalse(IntegrationMessage message)
|
||||
{
|
||||
var result = IntegrationHandlerResult.Succeed(message);
|
||||
|
||||
Assert.False(result.Retryable);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Data.EventIntegrations;
|
||||
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
|
||||
public class IntegrationMessageTests
|
||||
{
|
||||
@@ -1,12 +1,12 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations;
|
||||
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
|
||||
public class IntegrationOAuthStateTests
|
||||
{
|
||||
@@ -1,13 +1,13 @@
|
||||
#nullable enable
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations;
|
||||
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
|
||||
public class IntegrationTemplateContextTests
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Data.Organizations;
|
||||
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
|
||||
public class OrganizationIntegrationConfigurationDetailsTests
|
||||
{
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
|
||||
public class TestListenerConfiguration : IIntegrationListenerConfiguration
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Models.Teams;
|
||||
using Bit.Core.Dirt.Models.Data.Teams;
|
||||
using Microsoft.Bot.Connector.Authentication;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Data.Teams;
|
||||
namespace Bit.Core.Test.Dirt.Models.Data.Teams;
|
||||
|
||||
public class TeamsBotCredentialProviderTests
|
||||
{
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
using System.Text.Json;
|
||||
using Azure.Messaging.ServiceBus;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@@ -12,7 +13,7 @@ using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AzureServiceBusEventListenerServiceTests
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
using System.Text.Json;
|
||||
using Azure.Messaging.ServiceBus;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -11,7 +13,7 @@ using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AzureServiceBusIntegrationListenerServiceTests
|
||||
@@ -78,8 +80,10 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
var sutProvider = GetSutProvider();
|
||||
message.RetryCount = 0;
|
||||
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = false;
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message: message,
|
||||
category: IntegrationFailureCategory.AuthenticationFailed, // NOT retryable
|
||||
failureReason: "403");
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
@@ -89,6 +93,12 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
|
||||
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
|
||||
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());
|
||||
_logger.Received().Log(
|
||||
LogLevel.Warning,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - non-recoverable error or max retries exceeded.")),
|
||||
Arg.Any<Exception?>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -96,9 +106,10 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.RetryCount = _config.MaxRetries;
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message: message,
|
||||
category: IntegrationFailureCategory.TransientError, // Retryable
|
||||
failureReason: "403");
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
@@ -108,6 +119,12 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
|
||||
await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));
|
||||
await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());
|
||||
_logger.Received().Log(
|
||||
LogLevel.Warning,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - non-recoverable error or max retries exceeded.")),
|
||||
Arg.Any<Exception?>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -116,8 +133,10 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
var sutProvider = GetSutProvider();
|
||||
message.RetryCount = 0;
|
||||
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message: message,
|
||||
category: IntegrationFailureCategory.TransientError, // Retryable
|
||||
failureReason: "403");
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
@@ -133,7 +152,7 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var result = new IntegrationHandlerResult(true, message);
|
||||
var result = IntegrationHandlerResult.Succeed(message);
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
@@ -156,7 +175,7 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Unhandled error processing ASB message")),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Net;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@@ -11,7 +11,7 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class DatadogIntegrationHandlerTests
|
||||
@@ -51,7 +51,7 @@ public class DatadogIntegrationHandlerTests
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.Empty(result.FailureReason);
|
||||
Assert.Null(result.FailureReason);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(DatadogIntegrationHandler.HttpClientName))
|
||||
@@ -1,12 +1,13 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class EventIntegrationEventWriteServiceTests
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -19,7 +20,7 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class EventIntegrationHandlerTests
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -6,7 +7,7 @@ using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class EventRepositoryHandlerTests
|
||||
@@ -1,9 +1,9 @@
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
public class IntegrationFilterFactoryTests
|
||||
{
|
||||
@@ -1,13 +1,13 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
public class IntegrationFilterServiceTests
|
||||
{
|
||||
145
test/Core.Test/Dirt/Services/IntegrationHandlerTests.cs
Normal file
145
test/Core.Test/Dirt/Services/IntegrationHandlerTests.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System.Net;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
public class IntegrationHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage()
|
||||
{
|
||||
var sut = new TestIntegrationHandler();
|
||||
var expected = new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
|
||||
MessageId = "TestMessageId",
|
||||
OrganizationId = "TestOrganizationId",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RenderedTemplate = "Template",
|
||||
DelayUntilDate = null,
|
||||
RetryCount = 0
|
||||
};
|
||||
|
||||
var result = await sut.HandleAsync(expected.ToJson());
|
||||
var typedResult = Assert.IsType<IntegrationMessage<WebhookIntegrationConfigurationDetails>>(result.Message);
|
||||
|
||||
Assert.Equal(expected.MessageId, typedResult.MessageId);
|
||||
Assert.Equal(expected.OrganizationId, typedResult.OrganizationId);
|
||||
Assert.Equal(expected.Configuration, typedResult.Configuration);
|
||||
Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate);
|
||||
Assert.Equal(expected.IntegrationType, typedResult.IntegrationType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(HttpStatusCode.Unauthorized)]
|
||||
[InlineData(HttpStatusCode.Forbidden)]
|
||||
public void ClassifyHttpStatusCode_AuthenticationFailed(HttpStatusCode code)
|
||||
{
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.AuthenticationFailed,
|
||||
TestIntegrationHandler.Classify(code));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(HttpStatusCode.NotFound)]
|
||||
[InlineData(HttpStatusCode.Gone)]
|
||||
[InlineData(HttpStatusCode.MovedPermanently)]
|
||||
[InlineData(HttpStatusCode.TemporaryRedirect)]
|
||||
[InlineData(HttpStatusCode.PermanentRedirect)]
|
||||
public void ClassifyHttpStatusCode_ConfigurationError(HttpStatusCode code)
|
||||
{
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.ConfigurationError,
|
||||
TestIntegrationHandler.Classify(code));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyHttpStatusCode_TooManyRequests_IsRateLimited()
|
||||
{
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.RateLimited,
|
||||
TestIntegrationHandler.Classify(HttpStatusCode.TooManyRequests));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyHttpStatusCode_RequestTimeout_IsTransient()
|
||||
{
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.TransientError,
|
||||
TestIntegrationHandler.Classify(HttpStatusCode.RequestTimeout));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(HttpStatusCode.InternalServerError)]
|
||||
[InlineData(HttpStatusCode.BadGateway)]
|
||||
[InlineData(HttpStatusCode.GatewayTimeout)]
|
||||
public void ClassifyHttpStatusCode_Common5xx_AreTransient(HttpStatusCode code)
|
||||
{
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.TransientError,
|
||||
TestIntegrationHandler.Classify(code));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyHttpStatusCode_ServiceUnavailable_IsServiceUnavailable()
|
||||
{
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.ServiceUnavailable,
|
||||
TestIntegrationHandler.Classify(HttpStatusCode.ServiceUnavailable));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyHttpStatusCode_NotImplemented_IsPermanentFailure()
|
||||
{
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.PermanentFailure,
|
||||
TestIntegrationHandler.Classify(HttpStatusCode.NotImplemented));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FClassifyHttpStatusCode_Unhandled3xx_IsConfigurationError()
|
||||
{
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.ConfigurationError,
|
||||
TestIntegrationHandler.Classify(HttpStatusCode.Found));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyHttpStatusCode_Unhandled4xx_IsConfigurationError()
|
||||
{
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.ConfigurationError,
|
||||
TestIntegrationHandler.Classify(HttpStatusCode.BadRequest));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyHttpStatusCode_Unhandled5xx_IsServiceUnavailable()
|
||||
{
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.ServiceUnavailable,
|
||||
TestIntegrationHandler.Classify(HttpStatusCode.HttpVersionNotSupported));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyHttpStatusCode_UnknownCode_DefaultsToServiceUnavailable()
|
||||
{
|
||||
// cast an out-of-range value to ensure default path is stable
|
||||
Assert.Equal(
|
||||
IntegrationFailureCategory.ServiceUnavailable,
|
||||
TestIntegrationHandler.Classify((HttpStatusCode)799));
|
||||
}
|
||||
|
||||
private class TestIntegrationHandler : IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
public override Task<IntegrationHandlerResult> HandleAsync(
|
||||
IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
return Task.FromResult(IntegrationHandlerResult.Succeed(message: message));
|
||||
}
|
||||
|
||||
public static IntegrationFailureCategory Classify(HttpStatusCode code) => ClassifyHttpStatusCode(code);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
public class OrganizationIntegrationConfigurationValidatorTests
|
||||
{
|
||||
@@ -1,9 +1,10 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@@ -13,7 +14,7 @@ using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RabbitMqEventListenerServiceTests
|
||||
@@ -1,8 +1,10 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Text;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Core.Test.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@@ -13,7 +15,7 @@ using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RabbitMqIntegrationListenerServiceTests
|
||||
@@ -86,8 +88,10 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
new BasicProperties(),
|
||||
body: Encoding.UTF8.GetBytes(message.ToJson())
|
||||
);
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = false;
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message: message,
|
||||
category: IntegrationFailureCategory.AuthenticationFailed, // NOT retryable
|
||||
failureReason: "403");
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
@@ -105,7 +109,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
_logger.Received().Log(
|
||||
LogLevel.Warning,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Non-retryable failure")),
|
||||
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - non-retryable.")),
|
||||
Arg.Any<Exception?>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
|
||||
@@ -133,8 +137,10 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
new BasicProperties(),
|
||||
body: Encoding.UTF8.GetBytes(message.ToJson())
|
||||
);
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message: message,
|
||||
category: IntegrationFailureCategory.TransientError, // Retryable
|
||||
failureReason: "403");
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
@@ -151,7 +157,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
_logger.Received().Log(
|
||||
LogLevel.Warning,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Max retry attempts reached")),
|
||||
Arg.Is<object>(o => (o.ToString() ?? "").Contains("Integration failure - max retries exceeded.")),
|
||||
Arg.Any<Exception?>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
|
||||
@@ -179,9 +185,10 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
new BasicProperties(),
|
||||
body: Encoding.UTF8.GetBytes(message.ToJson())
|
||||
);
|
||||
var result = new IntegrationHandlerResult(false, message);
|
||||
result.Retryable = true;
|
||||
result.DelayUntilDate = _now.AddMinutes(1);
|
||||
var result = IntegrationHandlerResult.Fail(
|
||||
message: message,
|
||||
category: IntegrationFailureCategory.TransientError, // Retryable
|
||||
failureReason: "403");
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||
@@ -220,7 +227,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
||||
new BasicProperties(),
|
||||
body: Encoding.UTF8.GetBytes(message.ToJson())
|
||||
);
|
||||
var result = new IntegrationHandlerResult(true, message);
|
||||
var result = IntegrationHandlerResult.Succeed(message);
|
||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||
|
||||
await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);
|
||||
@@ -1,13 +1,14 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Models.Slack;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Models.Data.Slack;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SlackIntegrationHandlerTests
|
||||
@@ -110,7 +111,7 @@ public class SlackIntegrationHandlerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_NullResponse_ReturnsNonRetryableFailure()
|
||||
public async Task HandleAsync_NullResponse_ReturnsRetryableFailure()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
|
||||
@@ -126,7 +127,7 @@ public class SlackIntegrationHandlerTests
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.True(result.Retryable); // Null response is classified as TransientError (retryable)
|
||||
Assert.Equal("Slack response was null", result.FailureReason);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.MockedHttpClient;
|
||||
@@ -11,7 +11,7 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SlackServiceTests
|
||||
@@ -1,5 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Services;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Services;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@@ -8,7 +10,7 @@ using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class TeamsIntegrationHandlerTests
|
||||
@@ -42,9 +44,77 @@ public class TeamsIntegrationHandlerTests
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_ArgumentException_ReturnsConfigurationError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
|
||||
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.ThrowsAsync(new ArgumentException("argument error"));
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(IntegrationFailureCategory.ConfigurationError, result.Category);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_HttpExceptionNonRetryable_ReturnsFalseAndNotRetryable(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
public async Task HandleAsync_JsonException_ReturnsPermanentFailure(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
|
||||
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.ThrowsAsync(new JsonException("JSON error"));
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(IntegrationFailureCategory.PermanentFailure, result.Category);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_UriFormatException_ReturnsConfigurationError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
|
||||
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.ThrowsAsync(new UriFormatException("Bad URI"));
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(IntegrationFailureCategory.ConfigurationError, result.Category);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_HttpExceptionForbidden_ReturnsAuthenticationFailed(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
|
||||
@@ -62,6 +132,7 @@ public class TeamsIntegrationHandlerTests
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(IntegrationFailureCategory.AuthenticationFailed, result.Category);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
@@ -73,7 +144,7 @@ public class TeamsIntegrationHandlerTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_HttpExceptionRetryable_ReturnsFalseAndRetryable(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
public async Task HandleAsync_HttpExceptionTooManyRequests_ReturnsRateLimited(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
|
||||
@@ -92,6 +163,7 @@ public class TeamsIntegrationHandlerTests
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(IntegrationFailureCategory.RateLimited, result.Category);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
@@ -103,7 +175,7 @@ public class TeamsIntegrationHandlerTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_UnknownException_ReturnsFalseAndNotRetryable(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
public async Task HandleAsync_UnknownException_ReturnsTransientError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);
|
||||
@@ -114,7 +186,8 @@ public class TeamsIntegrationHandlerTests
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal(IntegrationFailureCategory.TransientError, result.Category);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(
|
||||
@@ -3,11 +3,11 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Models.Teams;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Models.Data.Teams;
|
||||
using Bit.Core.Dirt.Repositories;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.MockedHttpClient;
|
||||
@@ -15,7 +15,7 @@ using NSubstitute;
|
||||
using Xunit;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class TeamsServiceTests
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Dirt.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Dirt.Services.Implementations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@@ -10,7 +10,7 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
namespace Bit.Core.Test.Dirt.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class WebhookIntegrationHandlerTests
|
||||
@@ -51,7 +51,7 @@ public class WebhookIntegrationHandlerTests
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.Empty(result.FailureReason);
|
||||
Assert.Null(result.FailureReason);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
|
||||
@@ -79,7 +79,7 @@ public class WebhookIntegrationHandlerTests
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.Empty(result.FailureReason);
|
||||
Assert.Null(result.FailureReason);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Authorization;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.Authorization;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class KeyConnectorAuthorizationHandlerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UserCanUseKeyConnector_Success(
|
||||
User user,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations
|
||||
.Returns(new List<CurrentContextOrganization>());
|
||||
|
||||
var requirement = KeyConnectorOperations.Use;
|
||||
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UserAlreadyUsesKeyConnector_Fails(
|
||||
User user,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = true;
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations
|
||||
.Returns(new List<CurrentContextOrganization>());
|
||||
|
||||
var requirement = KeyConnectorOperations.Use;
|
||||
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UserIsOwner_Fails(
|
||||
User user,
|
||||
Guid organizationId,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
var organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = organizationId, Type = OrganizationUserType.Owner }
|
||||
};
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
|
||||
|
||||
var requirement = KeyConnectorOperations.Use;
|
||||
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UserIsAdmin_Fails(
|
||||
User user,
|
||||
Guid organizationId,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
var organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = organizationId, Type = OrganizationUserType.Admin }
|
||||
};
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
|
||||
|
||||
var requirement = KeyConnectorOperations.Use;
|
||||
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UserIsRegularMember_Success(
|
||||
User user,
|
||||
Guid organizationId,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
var organizations = new List<CurrentContextOrganization>
|
||||
{
|
||||
new() { Id = organizationId, Type = OrganizationUserType.User }
|
||||
};
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
|
||||
|
||||
var requirement = KeyConnectorOperations.Use;
|
||||
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_UnsupportedRequirement_ThrowsArgumentException(
|
||||
User user,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations
|
||||
.Returns(new List<CurrentContextOrganization>());
|
||||
|
||||
var unsupportedRequirement = new KeyConnectorOperationsRequirement("UnsupportedOperation");
|
||||
var context = new AuthorizationHandlerContext([unsupportedRequirement], claimsPrincipal, user);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(context));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Commands;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.Commands;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SetKeyConnectorKeyCommandTests
|
||||
{
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetKeyConnectorKeyForUserAsync_Success_SetsAccountKeys(
|
||||
User user,
|
||||
KeyConnectorKeysData data,
|
||||
SutProvider<SetKeyConnectorKeyCommand> sutProvider)
|
||||
{
|
||||
// Set up valid V2 encryption data
|
||||
if (data.AccountKeys!.SignatureKeyPair != null)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair.SignatureAlgorithm = "ed25519";
|
||||
}
|
||||
|
||||
var expectedAccountKeysData = data.AccountKeys.ToAccountKeysData();
|
||||
|
||||
// Arrange
|
||||
user.UsesKeyConnector = false;
|
||||
var currentContext = sutProvider.GetDependency<ICurrentContext>();
|
||||
var httpContext = Substitute.For<HttpContext>();
|
||||
httpContext.User.Returns(new ClaimsPrincipal());
|
||||
currentContext.HttpContext.Returns(httpContext);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())
|
||||
.Returns(AuthorizationResult.Success());
|
||||
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
var mockUpdateUserData = Substitute.For<UpdateUserData>();
|
||||
userRepository.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey!)
|
||||
.Returns(mockUpdateUserData);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data);
|
||||
|
||||
// Assert
|
||||
|
||||
userRepository
|
||||
.Received(1)
|
||||
.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey);
|
||||
|
||||
await userRepository
|
||||
.Received(1)
|
||||
.SetV2AccountCryptographicStateAsync(
|
||||
user.Id,
|
||||
Arg.Is<UserAccountKeysData>(data =>
|
||||
data.PublicKeyEncryptionKeyPairData.PublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.PublicKey &&
|
||||
data.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey &&
|
||||
data.PublicKeyEncryptionKeyPairData.SignedPublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey &&
|
||||
data.SignatureKeyPairData!.SignatureAlgorithm == expectedAccountKeysData.SignatureKeyPairData!.SignatureAlgorithm &&
|
||||
data.SignatureKeyPairData.WrappedSigningKey == expectedAccountKeysData.SignatureKeyPairData.WrappedSigningKey &&
|
||||
data.SignatureKeyPairData.VerifyingKey == expectedAccountKeysData.SignatureKeyPairData.VerifyingKey &&
|
||||
data.SecurityStateData!.SecurityState == expectedAccountKeysData.SecurityStateData!.SecurityState &&
|
||||
data.SecurityStateData.SecurityVersion == expectedAccountKeysData.SecurityStateData.SecurityVersion),
|
||||
Arg.Is<IEnumerable<UpdateUserData>>(actions =>
|
||||
actions.Count() == 1 && actions.First() == mockUpdateUserData));
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
|
||||
|
||||
await sutProvider.GetDependency<IAcceptOrgUserCommand>()
|
||||
.Received(1)
|
||||
.AcceptOrgUserByOrgSsoIdAsync(data.OrgIdentifier, user, sutProvider.GetDependency<IUserService>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetKeyConnectorKeyForUserAsync_UserCantUseKeyConnector_ThrowsException(
|
||||
User user,
|
||||
KeyConnectorKeysData data,
|
||||
SutProvider<SetKeyConnectorKeyCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.UsesKeyConnector = true;
|
||||
var currentContext = sutProvider.GetDependency<ICurrentContext>();
|
||||
var httpContext = Substitute.For<HttpContext>();
|
||||
httpContext.User.Returns(new ClaimsPrincipal());
|
||||
currentContext.HttpContext.Returns(httpContext);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())
|
||||
.Returns(AuthorizationResult.Failed());
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data));
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SetKeyConnectorUserKey(Arg.Any<Guid>(), Arg.Any<string>());
|
||||
|
||||
await sutProvider.GetDependency<IUserRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>(), Arg.Any<IEnumerable<UpdateUserData>>());
|
||||
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.LogUserEventAsync(Arg.Any<Guid>(), Arg.Any<EventType>());
|
||||
|
||||
await sutProvider.GetDependency<IAcceptOrgUserCommand>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.AcceptOrgUserByOrgSsoIdAsync(Arg.Any<string>(), Arg.Any<User>(), Arg.Any<IUserService>());
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||
@@ -242,4 +243,134 @@ public class UpgradeOrganizationPlanCommandTests
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
||||
public async Task UpgradePlan_WhenOrganizationIsMissingPublicAndPrivateKeys_Backfills(
|
||||
Organization organization,
|
||||
OrganizationUpgrade upgrade,
|
||||
string newPublicKey,
|
||||
string newPrivateKey,
|
||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||
{
|
||||
organization.PublicKey = null;
|
||||
organization.PrivateKey = null;
|
||||
|
||||
upgrade.Plan = PlanType.TeamsAnnually;
|
||||
upgrade.Keys = new PublicKeyEncryptionKeyPairData(
|
||||
wrappedPrivateKey: newPrivateKey,
|
||||
publicKey: newPublicKey);
|
||||
upgrade.AdditionalSeats = 10;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(upgrade.Plan)
|
||||
.Returns(MockPlans.Get(upgrade.Plan));
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 });
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(newPublicKey, organization.PublicKey);
|
||||
Assert.Equal(newPrivateKey, organization.PrivateKey);
|
||||
await sutProvider.GetDependency<IOrganizationService>()
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(organization);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
||||
public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotOverwriteWithNull(
|
||||
Organization organization,
|
||||
OrganizationUpgrade upgrade,
|
||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const string existingPublicKey = "existing-public-key";
|
||||
const string existingPrivateKey = "existing-private-key";
|
||||
|
||||
organization.PublicKey = existingPublicKey;
|
||||
organization.PrivateKey = existingPrivateKey;
|
||||
|
||||
upgrade.Plan = PlanType.TeamsAnnually;
|
||||
upgrade.Keys = null;
|
||||
upgrade.AdditionalSeats = 10;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(upgrade.Plan)
|
||||
.Returns(MockPlans.Get(upgrade.Plan));
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 });
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(existingPublicKey, organization.PublicKey);
|
||||
Assert.Equal(existingPrivateKey, organization.PrivateKey);
|
||||
await sutProvider.GetDependency<IOrganizationService>()
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(organization);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
||||
public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotBackfillWithNewKeys(
|
||||
Organization organization,
|
||||
OrganizationUpgrade upgrade,
|
||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
const string existingPublicKey = "existing-public-key";
|
||||
const string existingPrivateKey = "existing-private-key";
|
||||
const string newPublicKey = "new-public-key";
|
||||
const string newPrivateKey = "new-private-key";
|
||||
|
||||
organization.PublicKey = existingPublicKey;
|
||||
organization.PrivateKey = existingPrivateKey;
|
||||
|
||||
upgrade.Plan = PlanType.TeamsAnnually;
|
||||
upgrade.Keys = new PublicKeyEncryptionKeyPairData(
|
||||
wrappedPrivateKey: newPrivateKey,
|
||||
publicKey: newPublicKey);
|
||||
upgrade.AdditionalSeats = 10;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(upgrade.Plan)
|
||||
.Returns(MockPlans.Get(upgrade.Plan));
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 });
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(existingPublicKey, organization.PublicKey);
|
||||
Assert.Equal(existingPrivateKey, organization.PrivateKey);
|
||||
await sutProvider.GetDependency<IOrganizationService>()
|
||||
.Received(1)
|
||||
.ReplaceAndUpdateCacheAsync(organization);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ public class TestMailView : BaseMailView
|
||||
|
||||
public class TestMail : BaseMail<TestMailView>
|
||||
{
|
||||
public override string Subject { get; } = "Test Email";
|
||||
public override string Subject { get; set; } = "Test Email";
|
||||
}
|
||||
|
||||
@@ -25,11 +25,15 @@ using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Fido2NetLib;
|
||||
using Fido2NetLib.Objects;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using static Fido2NetLib.Fido2;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
@@ -594,6 +598,209 @@ public class UserServiceTests
|
||||
user.MasterPassword = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true)]
|
||||
[BitAutoData(false)]
|
||||
public async Task StartWebAuthnRegistrationAsync_BelowLimit_Succeeds(
|
||||
bool hasPremium, SutProvider<UserService> sutProvider, User user)
|
||||
{
|
||||
// Arrange - Non-premium user with 4 credentials (below limit of 5)
|
||||
SetupWebAuthnProvider(user, credentialCount: 4);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = new GlobalSettings.WebAuthnSettings
|
||||
{
|
||||
PremiumMaximumAllowedCredentials = 10,
|
||||
NonPremiumMaximumAllowedCredentials = 5
|
||||
};
|
||||
|
||||
user.Premium = hasPremium;
|
||||
user.Id = Guid.NewGuid();
|
||||
user.Email = "test@example.com";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns(new List<OrganizationUser>());
|
||||
|
||||
var mockFido2 = sutProvider.GetDependency<IFido2>();
|
||||
mockFido2.RequestNewCredential(
|
||||
Arg.Any<Fido2User>(),
|
||||
Arg.Any<List<PublicKeyCredentialDescriptor>>(),
|
||||
Arg.Any<AuthenticatorSelection>(),
|
||||
Arg.Any<AttestationConveyancePreference>())
|
||||
.Returns(new CredentialCreateOptions
|
||||
{
|
||||
Challenge = new byte[] { 1, 2, 3 },
|
||||
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
|
||||
User = new Fido2User
|
||||
{
|
||||
Id = user.Id.ToByteArray(),
|
||||
Name = user.Email,
|
||||
DisplayName = user.Name
|
||||
},
|
||||
PubKeyCredParams = new List<PubKeyCredParam>()
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.StartWebAuthnRegistrationAsync(user);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(user);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true)]
|
||||
[BitAutoData(false)]
|
||||
public async Task CompleteWebAuthRegistrationAsync_ExceedsLimit_ThrowsBadRequestException(bool hasPremium,
|
||||
SutProvider<UserService> sutProvider, User user, AuthenticatorAttestationRawResponse deviceResponse)
|
||||
{
|
||||
// Arrange - time-of-check/time-of-use scenario: user now has 10 credentials (at limit)
|
||||
SetupWebAuthnProviderWithPending(user, credentialCount: 10);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = new GlobalSettings.WebAuthnSettings
|
||||
{
|
||||
PremiumMaximumAllowedCredentials = 10,
|
||||
NonPremiumMaximumAllowedCredentials = 5
|
||||
};
|
||||
|
||||
user.Premium = hasPremium;
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns(new List<OrganizationUser>());
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CompleteWebAuthRegistrationAsync(user, 11, "NewKey", deviceResponse));
|
||||
|
||||
Assert.Equal("Maximum allowed WebAuthn credential count exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true)]
|
||||
[BitAutoData(false)]
|
||||
public async Task CompleteWebAuthRegistrationAsync_BelowLimit_Succeeds(bool hasPremium,
|
||||
SutProvider<UserService> sutProvider, User user, AuthenticatorAttestationRawResponse deviceResponse)
|
||||
{
|
||||
// Arrange - User has 4 credentials (below limit of 5)
|
||||
SetupWebAuthnProviderWithPending(user, credentialCount: 4);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().WebAuthn = new GlobalSettings.WebAuthnSettings
|
||||
{
|
||||
PremiumMaximumAllowedCredentials = 10,
|
||||
NonPremiumMaximumAllowedCredentials = 5
|
||||
};
|
||||
|
||||
user.Premium = hasPremium;
|
||||
user.Id = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns(new List<OrganizationUser>());
|
||||
|
||||
var mockFido2 = sutProvider.GetDependency<IFido2>();
|
||||
mockFido2.MakeNewCredentialAsync(
|
||||
Arg.Any<AuthenticatorAttestationRawResponse>(),
|
||||
Arg.Any<CredentialCreateOptions>(),
|
||||
Arg.Any<IsCredentialIdUniqueToUserAsyncDelegate>())
|
||||
.Returns(new CredentialMakeResult("ok", "", new AttestationVerificationSuccess
|
||||
{
|
||||
Aaguid = Guid.NewGuid(),
|
||||
Counter = 0,
|
||||
CredentialId = new byte[] { 1, 2, 3 },
|
||||
CredType = "public-key",
|
||||
PublicKey = new byte[] { 4, 5, 6 },
|
||||
Status = "ok",
|
||||
User = new Fido2User
|
||||
{
|
||||
Id = user.Id.ToByteArray(),
|
||||
Name = user.Email ?? "test@example.com",
|
||||
DisplayName = user.Name ?? "Test User"
|
||||
}
|
||||
}));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.CompleteWebAuthRegistrationAsync(user, 5, "NewKey", deviceResponse);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(user);
|
||||
}
|
||||
|
||||
private static void SetupWebAuthnProvider(User user, int credentialCount)
|
||||
{
|
||||
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||
var metadata = new Dictionary<string, object>();
|
||||
|
||||
// Add credentials as Key1, Key2, Key3, etc.
|
||||
for (int i = 1; i <= credentialCount; i++)
|
||||
{
|
||||
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
|
||||
{
|
||||
Name = $"Key {i}",
|
||||
Descriptor = new PublicKeyCredentialDescriptor(new byte[] { (byte)i }),
|
||||
PublicKey = new byte[] { (byte)i },
|
||||
UserHandle = new byte[] { (byte)i },
|
||||
SignatureCounter = 0,
|
||||
CredType = "public-key",
|
||||
RegDate = DateTime.UtcNow,
|
||||
AaGuid = Guid.NewGuid()
|
||||
};
|
||||
}
|
||||
|
||||
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider
|
||||
{
|
||||
Enabled = true,
|
||||
MetaData = metadata
|
||||
};
|
||||
|
||||
user.SetTwoFactorProviders(providers);
|
||||
}
|
||||
|
||||
private static void SetupWebAuthnProviderWithPending(User user, int credentialCount)
|
||||
{
|
||||
var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||
var metadata = new Dictionary<string, object>();
|
||||
|
||||
// Add existing credentials
|
||||
for (int i = 1; i <= credentialCount; i++)
|
||||
{
|
||||
metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData
|
||||
{
|
||||
Name = $"Key {i}",
|
||||
Descriptor = new PublicKeyCredentialDescriptor(new byte[] { (byte)i }),
|
||||
PublicKey = new byte[] { (byte)i },
|
||||
UserHandle = new byte[] { (byte)i },
|
||||
SignatureCounter = 0,
|
||||
CredType = "public-key",
|
||||
RegDate = DateTime.UtcNow,
|
||||
AaGuid = Guid.NewGuid()
|
||||
};
|
||||
}
|
||||
|
||||
// Add pending registration
|
||||
var pendingOptions = new CredentialCreateOptions
|
||||
{
|
||||
Challenge = new byte[] { 1, 2, 3 },
|
||||
Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""),
|
||||
User = new Fido2User
|
||||
{
|
||||
Id = user.Id.ToByteArray(),
|
||||
Name = user.Email ?? "test@example.com",
|
||||
DisplayName = user.Name ?? "Test User"
|
||||
},
|
||||
PubKeyCredParams = new List<PubKeyCredParam>()
|
||||
};
|
||||
metadata["pending"] = pendingOptions.ToJson();
|
||||
|
||||
providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider
|
||||
{
|
||||
Enabled = true,
|
||||
MetaData = metadata
|
||||
};
|
||||
|
||||
user.SetTwoFactorProviders(providers);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UserServiceSutProviderExtensions
|
||||
|
||||
169
test/Core.Test/Tools/Services/SendOwnerQueryTests.cs
Normal file
169
test/Core.Test/Tools/Services/SendOwnerQueryTests.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.SendFeatures.Queries;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.Services;
|
||||
|
||||
public class SendOwnerQueryTests
|
||||
{
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly SendOwnerQuery _sendOwnerQuery;
|
||||
private readonly Guid _currentUserId = Guid.NewGuid();
|
||||
private readonly ClaimsPrincipal _user;
|
||||
|
||||
public SendOwnerQueryTests()
|
||||
{
|
||||
_sendRepository = Substitute.For<ISendRepository>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_user = new ClaimsPrincipal();
|
||||
_userService.GetProperUserId(_user).Returns(_currentUserId);
|
||||
_sendOwnerQuery = new SendOwnerQuery(_sendRepository, _featureService, _userService);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WithValidSendOwnedByUser_ReturnsExpectedSend()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var expectedSend = CreateSend(sendId, _currentUserId);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(expectedSend);
|
||||
|
||||
// Act
|
||||
var result = await _sendOwnerQuery.Get(sendId, _user);
|
||||
|
||||
// Assert
|
||||
Assert.Same(expectedSend, result);
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WithNonExistentSend_ThrowsNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
_sendRepository.GetByIdAsync(sendId).Returns((Send?)null);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sendOwnerQuery.Get(sendId, _user));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WithSendOwnedByDifferentUser_ThrowsNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var differentUserId = Guid.NewGuid();
|
||||
var send = CreateSend(sendId, differentUserId);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sendOwnerQuery.Get(sendId, _user));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WithNullCurrentUserId_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = CreateSend(sendId, _currentUserId);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
var nullUser = new ClaimsPrincipal();
|
||||
_userService.GetProperUserId(nullUser).Returns((Guid?)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sendOwnerQuery.Get(sendId, nullUser));
|
||||
Assert.Equal("invalid user.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOwned_WithFeatureFlagEnabled_ReturnsAllSends()
|
||||
{
|
||||
// Arrange
|
||||
var sends = new List<Send>
|
||||
{
|
||||
CreateSend(Guid.NewGuid(), _currentUserId, emails: null),
|
||||
CreateSend(Guid.NewGuid(), _currentUserId, emails: "test@example.com"),
|
||||
CreateSend(Guid.NewGuid(), _currentUserId, emails: "other@example.com")
|
||||
};
|
||||
_sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(sends);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sendOwnerQuery.GetOwned(_user);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Contains(sends[0], result);
|
||||
Assert.Contains(sends[1], result);
|
||||
Assert.Contains(sends[2], result);
|
||||
await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId);
|
||||
_featureService.Received(1).IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOwned_WithFeatureFlagDisabled_FiltersOutEmailOtpSends()
|
||||
{
|
||||
// Arrange
|
||||
var sendWithoutEmails = CreateSend(Guid.NewGuid(), _currentUserId, emails: null);
|
||||
var sendWithEmails = CreateSend(Guid.NewGuid(), _currentUserId, emails: "test@example.com");
|
||||
var sends = new List<Send> { sendWithoutEmails, sendWithEmails };
|
||||
_sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(sends);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await _sendOwnerQuery.GetOwned(_user);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Contains(sendWithoutEmails, result);
|
||||
Assert.DoesNotContain(sendWithEmails, result);
|
||||
await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId);
|
||||
_featureService.Received(1).IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOwned_WithNullCurrentUserId_ThrowsBadRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var nullUser = new ClaimsPrincipal();
|
||||
_userService.GetProperUserId(nullUser).Returns((Guid?)null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sendOwnerQuery.GetOwned(nullUser));
|
||||
Assert.Equal("invalid user.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOwned_WithEmptyCollection_ReturnsEmptyCollection()
|
||||
{
|
||||
// Arrange
|
||||
var emptySends = new List<Send>();
|
||||
_sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(emptySends);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sendOwnerQuery.GetOwned(_user);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId);
|
||||
}
|
||||
|
||||
private static Send CreateSend(Guid id, Guid userId, string? emails = null)
|
||||
{
|
||||
return new Send
|
||||
{
|
||||
Id = id,
|
||||
UserId = userId,
|
||||
Emails = emails
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Dirt.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
@@ -12,7 +12,6 @@ internal class OrganizationCipher : ICustomization
|
||||
{
|
||||
fixture.Customize<Cipher>(composer => composer
|
||||
.With(c => c.OrganizationId, OrganizationId ?? Guid.NewGuid())
|
||||
.Without(c => c.ArchivedDate)
|
||||
.Without(c => c.UserId));
|
||||
fixture.Customize<CipherDetails>(composer => composer
|
||||
.With(c => c.OrganizationId, Guid.NewGuid())
|
||||
@@ -28,7 +27,6 @@ internal class UserCipher : ICustomization
|
||||
{
|
||||
fixture.Customize<Cipher>(composer => composer
|
||||
.With(c => c.UserId, UserId ?? Guid.NewGuid())
|
||||
.Without(c => c.ArchivedDate)
|
||||
.Without(c => c.OrganizationId));
|
||||
fixture.Customize<CipherDetails>(composer => composer
|
||||
.With(c => c.UserId, Guid.NewGuid())
|
||||
|
||||
@@ -16,16 +16,15 @@ namespace Bit.Core.Test.Vault.Commands;
|
||||
public class ArchiveCiphersCommandTest
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(true, false, 1, 1, 1)]
|
||||
[BitAutoData(false, false, 1, 0, 1)]
|
||||
[BitAutoData(false, true, 1, 0, 1)]
|
||||
[BitAutoData(true, true, 1, 0, 1)]
|
||||
public async Task ArchiveAsync_Works(
|
||||
bool isEditable, bool hasOrganizationId,
|
||||
[BitAutoData(true, 1, 1, 1)]
|
||||
[BitAutoData(false, 1, 0, 1)]
|
||||
[BitAutoData(false, 1, 0, 1)]
|
||||
[BitAutoData(true, 1, 0, 1)]
|
||||
public async Task ArchiveManyAsync_Works(
|
||||
bool hasOrganizationId,
|
||||
int cipherRepoCalls, int resultCountFromQuery, int pushNotificationsCalls,
|
||||
SutProvider<ArchiveCiphersCommand> sutProvider, CipherDetails cipher, User user)
|
||||
{
|
||||
cipher.Edit = isEditable;
|
||||
cipher.OrganizationId = hasOrganizationId ? Guid.NewGuid() : null;
|
||||
|
||||
var cipherList = new List<CipherDetails> { cipher };
|
||||
@@ -46,4 +45,33 @@ public class ArchiveCiphersCommandTest
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(pushNotificationsCalls)
|
||||
.PushSyncCiphersAsync(user.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ArchiveManyAsync_SetsArchivedDateOnReturnedCiphers(
|
||||
SutProvider<ArchiveCiphersCommand> sutProvider,
|
||||
CipherDetails cipher,
|
||||
User user)
|
||||
{
|
||||
// Allow organization cipher to be archived in this test
|
||||
cipher.OrganizationId = Guid.Parse("3f2504e0-4f89-11d3-9a0c-0305e82c3301");
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<CipherDetails> { cipher });
|
||||
|
||||
var repoRevisionDate = DateTime.UtcNow;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.ArchiveAsync(Arg.Any<IEnumerable<Guid>>(), user.Id)
|
||||
.Returns(repoRevisionDate);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ArchiveManyAsync(new[] { cipher.Id }, user.Id);
|
||||
|
||||
// Assert
|
||||
var archivedCipher = Assert.Single(result);
|
||||
Assert.Equal(repoRevisionDate, archivedCipher.RevisionDate);
|
||||
Assert.Equal(repoRevisionDate, archivedCipher.ArchivedDate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,16 +16,15 @@ namespace Bit.Core.Test.Vault.Commands;
|
||||
public class UnarchiveCiphersCommandTest
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(true, false, 1, 1, 1)]
|
||||
[BitAutoData(false, false, 1, 0, 1)]
|
||||
[BitAutoData(false, true, 1, 0, 1)]
|
||||
[BitAutoData(true, true, 1, 1, 1)]
|
||||
[BitAutoData(true, 1, 1, 1)]
|
||||
[BitAutoData(false, 1, 0, 1)]
|
||||
[BitAutoData(false, 1, 0, 1)]
|
||||
[BitAutoData(true, 1, 1, 1)]
|
||||
public async Task UnarchiveAsync_Works(
|
||||
bool isEditable, bool hasOrganizationId,
|
||||
bool hasOrganizationId,
|
||||
int cipherRepoCalls, int resultCountFromQuery, int pushNotificationsCalls,
|
||||
SutProvider<UnarchiveCiphersCommand> sutProvider, CipherDetails cipher, User user)
|
||||
{
|
||||
cipher.Edit = isEditable;
|
||||
cipher.OrganizationId = hasOrganizationId ? Guid.NewGuid() : null;
|
||||
|
||||
var cipherList = new List<CipherDetails> { cipher };
|
||||
@@ -46,4 +45,33 @@ public class UnarchiveCiphersCommandTest
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(pushNotificationsCalls)
|
||||
.PushSyncCiphersAsync(user.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UnarchiveAsync_ClearsArchivedDateOnReturnedCiphers(
|
||||
SutProvider<UnarchiveCiphersCommand> sutProvider,
|
||||
CipherDetails cipher,
|
||||
User user)
|
||||
{
|
||||
cipher.OrganizationId = null;
|
||||
cipher.ArchivedDate = DateTime.UtcNow;
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<CipherDetails> { cipher });
|
||||
|
||||
var repoRevisionDate = DateTime.UtcNow.AddMinutes(1);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.UnarchiveAsync(Arg.Any<IEnumerable<Guid>>(), user.Id)
|
||||
.Returns(repoRevisionDate);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.UnarchiveManyAsync(new[] { cipher.Id }, user.Id);
|
||||
|
||||
// Assert
|
||||
var unarchivedCipher = Assert.Single(result);
|
||||
Assert.Equal(repoRevisionDate, unarchivedCipher.RevisionDate);
|
||||
Assert.Null(unarchivedCipher.ArchivedDate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1190,6 +1190,7 @@ public class CipherServiceTests
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
|
||||
.Returns(new Organization
|
||||
{
|
||||
UsePolicies = true,
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
MaxStorageGb = 100
|
||||
});
|
||||
@@ -1206,6 +1207,140 @@ public class CipherServiceTests
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ShareManyAsync_StorageLimitBypass_Passes(SutProvider<CipherService> sutProvider,
|
||||
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
|
||||
.Returns(new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
UsePolicies = true,
|
||||
MaxStorageGb = 3,
|
||||
Storage = 3221225472 // 3 GB used, so 0 remaining
|
||||
});
|
||||
ciphers.FirstOrDefault().Attachments =
|
||||
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
|
||||
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";
|
||||
|
||||
var cipherInfos = ciphers.Select(c => (c,
|
||||
(DateTime?)c.RevisionDate));
|
||||
var sharingUserId = ciphers.First().UserId.Value;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(sharingUserId)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Enabled,
|
||||
[new PolicyDetails
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
PolicyType = PolicyType.OrganizationDataOwnership,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
|
||||
}]));
|
||||
|
||||
await sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId);
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync(sharingUserId,
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ShareManyAsync_StorageLimit_Enforced(SutProvider<CipherService> sutProvider,
|
||||
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
|
||||
.Returns(new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
UsePolicies = true,
|
||||
MaxStorageGb = 3,
|
||||
Storage = 3221225472 // 3 GB used, so 0 remaining
|
||||
});
|
||||
ciphers.FirstOrDefault().Attachments =
|
||||
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
|
||||
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";
|
||||
|
||||
var cipherInfos = ciphers.Select(c => (c,
|
||||
(DateTime?)c.RevisionDate));
|
||||
var sharingUserId = ciphers.First().UserId.Value;
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(sharingUserId)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, []));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId)
|
||||
);
|
||||
Assert.Contains("Not enough storage available for this organization.", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpdateCiphersAsync(sharingUserId,
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ShareManyAsync_StorageLimit_Enforced_WhenFeatureFlagDisabled(SutProvider<CipherService> sutProvider,
|
||||
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
|
||||
.Returns(new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
UsePolicies = true,
|
||||
MaxStorageGb = 3,
|
||||
Storage = 3221225472 // 3 GB used, so 0 remaining
|
||||
});
|
||||
ciphers.FirstOrDefault().Attachments =
|
||||
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
|
||||
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";
|
||||
|
||||
var cipherInfos = ciphers.Select(c => (c,
|
||||
(DateTime?)c.RevisionDate));
|
||||
var sharingUserId = ciphers.First().UserId.Value;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId)
|
||||
);
|
||||
Assert.Contains("Not enough storage available for this organization.", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpdateCiphersAsync(sharingUserId,
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ShareManyAsync_StorageLimit_Enforced_WhenUsePoliciesDisabled(SutProvider<CipherService> sutProvider,
|
||||
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
|
||||
.Returns(new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
UsePolicies = false,
|
||||
MaxStorageGb = 3,
|
||||
Storage = 3221225472 // 3 GB used, so 0 remaining
|
||||
});
|
||||
ciphers.FirstOrDefault().Attachments =
|
||||
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
|
||||
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";
|
||||
|
||||
var cipherInfos = ciphers.Select(c => (c,
|
||||
(DateTime?)c.RevisionDate));
|
||||
var sharingUserId = ciphers.First().UserId.Value;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(true);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId)
|
||||
);
|
||||
Assert.Contains("Not enough storage available for this organization.", exception.Message);
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpdateCiphersAsync(sharingUserId,
|
||||
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
|
||||
}
|
||||
|
||||
private class SaveDetailsAsyncDependencies
|
||||
{
|
||||
public CipherDetails CipherDetails { get; set; }
|
||||
@@ -1215,12 +1350,12 @@ public class CipherServiceTests
|
||||
private static SaveDetailsAsyncDependencies GetSaveDetailsAsyncDependencies(
|
||||
SutProvider<CipherService> sutProvider,
|
||||
string newPassword,
|
||||
bool viewPassword,
|
||||
bool editPermission,
|
||||
bool permission,
|
||||
string? key = null,
|
||||
string? totp = null,
|
||||
CipherLoginFido2CredentialData[]? passkeys = null,
|
||||
CipherFieldData[]? fields = null
|
||||
CipherFieldData[]? fields = null,
|
||||
string? existingKey = "OriginalKey"
|
||||
)
|
||||
{
|
||||
var cipherDetails = new CipherDetails
|
||||
@@ -1233,13 +1368,22 @@ public class CipherServiceTests
|
||||
Key = key,
|
||||
};
|
||||
|
||||
var newLoginData = new CipherLoginData { Username = "user", Password = newPassword, Totp = totp, Fido2Credentials = passkeys, Fields = fields };
|
||||
var newLoginData = new CipherLoginData
|
||||
{
|
||||
Username = "user",
|
||||
Password = newPassword,
|
||||
Totp = totp,
|
||||
Fido2Credentials = passkeys,
|
||||
Fields = fields
|
||||
};
|
||||
|
||||
cipherDetails.Data = JsonSerializer.Serialize(newLoginData);
|
||||
|
||||
var existingCipher = new Cipher
|
||||
{
|
||||
Id = cipherDetails.Id,
|
||||
Type = CipherType.Login,
|
||||
Key = existingKey,
|
||||
Data = JsonSerializer.Serialize(
|
||||
new CipherLoginData
|
||||
{
|
||||
@@ -1261,7 +1405,14 @@ public class CipherServiceTests
|
||||
|
||||
var permissions = new Dictionary<Guid, OrganizationCipherPermission>
|
||||
{
|
||||
{ cipherDetails.Id, new OrganizationCipherPermission { ViewPassword = viewPassword, Edit = editPermission } }
|
||||
{
|
||||
cipherDetails.Id,
|
||||
new OrganizationCipherPermission
|
||||
{
|
||||
ViewPassword = permission,
|
||||
Edit = permission
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>()
|
||||
@@ -1278,7 +1429,7 @@ public class CipherServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_PasswordNotChangedWithoutViewPasswordPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: true);
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false);
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
@@ -1294,7 +1445,7 @@ public class CipherServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_PasswordNotChangedWithoutEditPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false);
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false);
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
@@ -1310,7 +1461,7 @@ public class CipherServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_PasswordChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true);
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true);
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
@@ -1326,7 +1477,11 @@ public class CipherServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_CipherKeyChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, "NewKey");
|
||||
var deps = GetSaveDetailsAsyncDependencies(
|
||||
sutProvider,
|
||||
newPassword: "NewPassword",
|
||||
permission: true,
|
||||
key: "NewKey");
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
@@ -1336,27 +1491,40 @@ public class CipherServiceTests
|
||||
true);
|
||||
|
||||
Assert.Equal("NewKey", deps.CipherDetails.Key);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received()
|
||||
.ReplaceAsync(Arg.Is<CipherDetails>(c => c.Id == deps.CipherDetails.Id && c.Key == "NewKey"));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_CipherKeyChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
public async Task SaveDetailsAsync_CipherKeyNotChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, "NewKey");
|
||||
var deps = GetSaveDetailsAsyncDependencies(
|
||||
sutProvider,
|
||||
newPassword: "NewPassword",
|
||||
permission: false,
|
||||
key: "NewKey"
|
||||
);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
deps.CipherDetails.UserId.Value,
|
||||
deps.CipherDetails.RevisionDate,
|
||||
null,
|
||||
true));
|
||||
true);
|
||||
|
||||
Assert.Contains("do not have permission", exception.Message);
|
||||
Assert.Equal("OriginalKey", deps.CipherDetails.Key);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received()
|
||||
.ReplaceAsync(Arg.Is<CipherDetails>(c => c.Id == deps.CipherDetails.Id && c.Key == "OriginalKey"));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_TotpChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, totp: "NewTotp");
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, totp: "NewTotp");
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
@@ -1372,7 +1540,7 @@ public class CipherServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveDetailsAsync_TotpChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, totp: "NewTotp");
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, totp: "NewTotp");
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
@@ -1397,7 +1565,7 @@ public class CipherServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, passkeys: passkeys);
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, passkeys: passkeys);
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
@@ -1422,7 +1590,7 @@ public class CipherServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, passkeys: passkeys);
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, passkeys: passkeys);
|
||||
|
||||
await deps.SutProvider.Sut.SaveDetailsAsync(
|
||||
deps.CipherDetails,
|
||||
@@ -1439,7 +1607,7 @@ public class CipherServiceTests
|
||||
[BitAutoData]
|
||||
public async Task SaveDetailsAsync_HiddenFieldsChangedWithoutPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: false, fields:
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, fields:
|
||||
[
|
||||
new CipherFieldData
|
||||
{
|
||||
@@ -1464,7 +1632,7 @@ public class CipherServiceTests
|
||||
[BitAutoData]
|
||||
public async Task SaveDetailsAsync_HiddenFieldsChangedWithPermission(string _, SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, fields:
|
||||
var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, fields:
|
||||
[
|
||||
new CipherFieldData
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -9,6 +10,7 @@ using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Events.Controllers;
|
||||
using Bit.Events.Models;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
|
||||
@@ -21,6 +23,7 @@ public class CollectControllerTests
|
||||
private readonly IEventService _eventService;
|
||||
private readonly ICipherRepository _cipherRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public CollectControllerTests()
|
||||
{
|
||||
@@ -28,12 +31,14 @@ public class CollectControllerTests
|
||||
_eventService = Substitute.For<IEventService>();
|
||||
_cipherRepository = Substitute.For<ICipherRepository>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
|
||||
_sut = new CollectController(
|
||||
_currentContext,
|
||||
_eventService,
|
||||
_cipherRepository,
|
||||
_organizationRepository
|
||||
_organizationRepository,
|
||||
_organizationUserRepository
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,6 +79,32 @@ public class CollectControllerTests
|
||||
await _eventService.Received(1).LogUserEventAsync(userId, EventType.User_ClientExportedVault, eventDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(EventType.Organization_ItemOrganization_Accepted)]
|
||||
[BitAutoData(EventType.Organization_ItemOrganization_Declined)]
|
||||
public async Task Post_Organization_ItemOrganization_LogsOrganizationUserEvent(
|
||||
EventType type, Guid userId, Guid orgId, OrganizationUser orgUser)
|
||||
{
|
||||
_currentContext.UserId.Returns(userId);
|
||||
orgUser.OrganizationId = orgId;
|
||||
_organizationUserRepository.GetByOrganizationAsync(orgId, userId).Returns(orgUser);
|
||||
var eventDate = DateTime.UtcNow;
|
||||
var events = new List<EventModel>
|
||||
{
|
||||
new EventModel
|
||||
{
|
||||
Type = type,
|
||||
OrganizationId = orgId,
|
||||
Date = eventDate
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _sut.Post(events);
|
||||
|
||||
Assert.IsType<OkResult>(result);
|
||||
await _eventService.Received(1).LogOrganizationUserEventAsync(orgUser, type, eventDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[AutoData]
|
||||
public async Task Post_CipherAutofilled_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||
|
||||
@@ -95,6 +95,7 @@ public static class OrganizationTestHelpers
|
||||
SyncSeats = false,
|
||||
UseAutomaticUserConfirmation = true,
|
||||
UsePhishingBlocker = true,
|
||||
UseDisableSmAdsForUsers = true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -144,4 +144,69 @@ public class CollectionRepositoryReplaceTests
|
||||
await userRepository.DeleteAsync(user);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task ReplaceAsync_WhenNotPassingGroupsOrUsers_DoesNotDeleteAccess(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var user1 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1);
|
||||
|
||||
var user2 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2);
|
||||
|
||||
var group1 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
var group2 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
await collectionRepository.CreateAsync(collection,
|
||||
[
|
||||
new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false },
|
||||
]
|
||||
);
|
||||
|
||||
// Act
|
||||
collection.Name = "Updated Collection Name";
|
||||
|
||||
await collectionRepository.ReplaceAsync(collection, null, null);
|
||||
|
||||
// Assert
|
||||
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||
|
||||
Assert.NotNull(actualCollection);
|
||||
Assert.Equal("Updated Collection Name", actualCollection.Name);
|
||||
|
||||
var groups = actualAccess.Groups.ToArray();
|
||||
Assert.Equal(2, groups.Length);
|
||||
Assert.Single(groups, g => g.Id == group1.Id && g.Manage && g.HidePasswords && !g.ReadOnly);
|
||||
Assert.Single(groups, g => g.Id == group2.Id && !g.Manage && !g.HidePasswords && g.ReadOnly);
|
||||
|
||||
var users = actualAccess.Users.ToArray();
|
||||
|
||||
Assert.Equal(2, users.Length);
|
||||
Assert.Single(users, u => u.Id == orgUser1.Id && u.Manage && !u.HidePasswords && u.ReadOnly);
|
||||
Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && u.HidePasswords && !u.ReadOnly);
|
||||
|
||||
// Clean up data
|
||||
await userRepository.DeleteAsync(user1);
|
||||
await userRepository.DeleteAsync(user2);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,6 +675,7 @@ public class OrganizationUserRepositoryTests
|
||||
UseRiskInsights = false,
|
||||
UseAdminSponsoredFamilies = false,
|
||||
UsePhishingBlocker = false,
|
||||
UseDisableSmAdsForUsers = false,
|
||||
});
|
||||
|
||||
var organizationDomain = new OrganizationDomain
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Infrastructure.IntegrationTest.AdminConsole;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.Repositories;
|
||||
@@ -500,4 +502,54 @@ public class UserRepositoryTests
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task SetKeyConnectorUserKey_UpdatesUserKey(IUserRepository userRepository, Database database)
|
||||
{
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
|
||||
const string keyConnectorWrappedKey = "key-connector-wrapped-user-key";
|
||||
|
||||
var setKeyConnectorUserKeyDelegate = userRepository.SetKeyConnectorUserKey(user.Id, keyConnectorWrappedKey);
|
||||
|
||||
await RunUpdateUserDataAsync(setKeyConnectorUserKeyDelegate, database);
|
||||
|
||||
var updatedUser = await userRepository.GetByIdAsync(user.Id);
|
||||
|
||||
Assert.NotNull(updatedUser);
|
||||
Assert.Equal(keyConnectorWrappedKey, updatedUser.Key);
|
||||
Assert.True(updatedUser.UsesKeyConnector);
|
||||
Assert.Equal(KdfType.Argon2id, updatedUser.Kdf);
|
||||
Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, updatedUser.KdfIterations);
|
||||
Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, updatedUser.KdfMemory);
|
||||
Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, updatedUser.KdfParallelism);
|
||||
Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));
|
||||
Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
private static async Task RunUpdateUserDataAsync(UpdateUserData task, Database database)
|
||||
{
|
||||
if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf)
|
||||
{
|
||||
await using var connection = new SqlConnection(database.ConnectionString);
|
||||
connection.Open();
|
||||
|
||||
await using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
await task(connection, transaction);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await task();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1207,10 +1207,110 @@ public class CipherRepositoryTests
|
||||
// Act
|
||||
await sutRepository.ArchiveAsync(new List<Guid> { cipher.Id }, user.Id);
|
||||
|
||||
// Assert
|
||||
var archivedCipher = await sutRepository.GetByIdAsync(cipher.Id, user.Id);
|
||||
Assert.NotNull(archivedCipher);
|
||||
Assert.NotNull(archivedCipher.ArchivedDate);
|
||||
// Assert – per-user view should show an archive date
|
||||
var archivedCipherForUser = await sutRepository.GetByIdAsync(cipher.Id, user.Id);
|
||||
Assert.NotNull(archivedCipherForUser);
|
||||
Assert.NotNull(archivedCipherForUser.ArchivedDate);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ArchiveAsync_IsPerUserForSharedCipher(
|
||||
ICipherRepository cipherRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
ICollectionCipherRepository collectionCipherRepository)
|
||||
{
|
||||
// Arrange: two users in the same org, both with access to the same cipher
|
||||
var user1 = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User 1",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
var user2 = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User 2",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
var org = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Organization",
|
||||
BillingEmail = user1.Email,
|
||||
Plan = "Test",
|
||||
});
|
||||
|
||||
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
UserId = user1.Id,
|
||||
OrganizationId = org.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.Owner,
|
||||
});
|
||||
|
||||
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
UserId = user2.Id,
|
||||
OrganizationId = org.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.User,
|
||||
});
|
||||
|
||||
var sharedCollection = await collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
Name = "Shared Collection",
|
||||
OrganizationId = org.Id,
|
||||
});
|
||||
|
||||
var cipher = await cipherRepository.CreateAsync(new Cipher
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
OrganizationId = org.Id,
|
||||
Data = "",
|
||||
});
|
||||
|
||||
await collectionCipherRepository.UpdateCollectionsForAdminAsync(
|
||||
cipher.Id,
|
||||
org.Id,
|
||||
new List<Guid> { sharedCollection.Id });
|
||||
|
||||
// Give both org users access to the shared collection
|
||||
await collectionRepository.UpdateUsersAsync(sharedCollection.Id, new List<CollectionAccessSelection>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = orgUser1.Id,
|
||||
HidePasswords = false,
|
||||
ReadOnly = false,
|
||||
Manage = true,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = orgUser2.Id,
|
||||
HidePasswords = false,
|
||||
ReadOnly = false,
|
||||
Manage = true,
|
||||
},
|
||||
});
|
||||
|
||||
// Act: user1 archives the shared cipher
|
||||
await cipherRepository.ArchiveAsync(new List<Guid> { cipher.Id }, user1.Id);
|
||||
|
||||
// Assert: user1 sees it as archived
|
||||
var cipherForUser1 = await cipherRepository.GetByIdAsync(cipher.Id, user1.Id);
|
||||
Assert.NotNull(cipherForUser1);
|
||||
Assert.NotNull(cipherForUser1.ArchivedDate);
|
||||
|
||||
// Assert: user2 still sees it as *not* archived
|
||||
var cipherForUser2 = await cipherRepository.GetByIdAsync(cipher.Id, user2.Id);
|
||||
Assert.NotNull(cipherForUser2);
|
||||
Assert.Null(cipherForUser2.ArchivedDate);
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
|
||||
@@ -154,6 +154,7 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
||||
|
||||
// Web push notifications
|
||||
{ "globalSettings:webPush:vapidPublicKey", "BGBtAM0bU3b5jsB14IjBYarvJZ6rWHilASLudTTYDDBi7a-3kebo24Yus_xYeOMZ863flAXhFAbkL6GVSrxgErg" },
|
||||
{ "globalSettings:launchDarkly:flagValues:web-push", "true" },
|
||||
};
|
||||
|
||||
// Some database drivers modify the connection string
|
||||
|
||||
Reference in New Issue
Block a user