mirror of
https://github.com/bitwarden/server
synced 2026-01-19 00:43:47 +00:00
Merge branch 'main' into tools/pm-21918/send-authentication-commands
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Authorization;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RecoverAccountAuthorizationHandlerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_CurrentUserIsProvider_TargetUserNotProvider_Authorized(
|
||||
SutProvider<RecoverAccountAuthorizationHandler> sutProvider,
|
||||
[OrganizationUser] OrganizationUser targetOrganizationUser,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
// Arrange
|
||||
var context = new AuthorizationHandlerContext(
|
||||
[new RecoverAccountAuthorizationRequirement()],
|
||||
claimsPrincipal,
|
||||
targetOrganizationUser);
|
||||
|
||||
MockOrganizationClaims(sutProvider, claimsPrincipal, targetOrganizationUser, null);
|
||||
MockCurrentUserIsProvider(sutProvider, claimsPrincipal, targetOrganizationUser);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_CurrentUserIsNotMemberOrProvider_NotAuthorized(
|
||||
SutProvider<RecoverAccountAuthorizationHandler> sutProvider,
|
||||
[OrganizationUser] OrganizationUser targetOrganizationUser,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
// Arrange
|
||||
var context = new AuthorizationHandlerContext(
|
||||
[new RecoverAccountAuthorizationRequirement()],
|
||||
claimsPrincipal,
|
||||
targetOrganizationUser);
|
||||
|
||||
MockOrganizationClaims(sutProvider, claimsPrincipal, targetOrganizationUser, null);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
AssertFailed(context, RecoverAccountAuthorizationHandler.FailureReason);
|
||||
}
|
||||
|
||||
// Pairing of CurrentContextOrganization (current user permissions) and target user role
|
||||
// Read this as: a ___ can recover the account for a ___
|
||||
public static IEnumerable<object[]> AuthorizedRoleCombinations => new object[][]
|
||||
{
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Owner }, OrganizationUserType.Owner],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Owner }, OrganizationUserType.Admin],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Owner }, OrganizationUserType.Custom],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Owner }, OrganizationUserType.User],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Admin }, OrganizationUserType.Admin],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Admin }, OrganizationUserType.Custom],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Admin }, OrganizationUserType.User],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Custom, Permissions = new Permissions { ManageResetPassword = true}}, OrganizationUserType.Custom],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Custom, Permissions = new Permissions { ManageResetPassword = true}}, OrganizationUserType.User],
|
||||
};
|
||||
|
||||
[Theory, BitMemberAutoData(nameof(AuthorizedRoleCombinations))]
|
||||
public async Task AuthorizeMemberAsync_RecoverEqualOrLesserRoles_TargetUserNotProvider_Authorized(
|
||||
CurrentContextOrganization currentContextOrganization,
|
||||
OrganizationUserType targetOrganizationUserType,
|
||||
SutProvider<RecoverAccountAuthorizationHandler> sutProvider,
|
||||
[OrganizationUser] OrganizationUser targetOrganizationUser,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
// Arrange
|
||||
targetOrganizationUser.Type = targetOrganizationUserType;
|
||||
currentContextOrganization.Id = targetOrganizationUser.OrganizationId;
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
[new RecoverAccountAuthorizationRequirement()],
|
||||
claimsPrincipal,
|
||||
targetOrganizationUser);
|
||||
|
||||
MockOrganizationClaims(sutProvider, claimsPrincipal, targetOrganizationUser, currentContextOrganization);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
// Pairing of CurrentContextOrganization (current user permissions) and target user role
|
||||
// Read this as: a ___ cannot recover the account for a ___
|
||||
public static IEnumerable<object[]> UnauthorizedRoleCombinations => new object[][]
|
||||
{
|
||||
// These roles should fail because you cannot recover a greater role
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Admin }, OrganizationUserType.Owner],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Custom, Permissions = new Permissions { ManageResetPassword = true}}, OrganizationUserType.Owner],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Custom, Permissions = new Permissions { ManageResetPassword = true} }, OrganizationUserType.Admin],
|
||||
|
||||
// These roles are never authorized to recover any account
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.User }, OrganizationUserType.Owner],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.User }, OrganizationUserType.Admin],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.User }, OrganizationUserType.Custom],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.User }, OrganizationUserType.User],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Custom }, OrganizationUserType.Owner],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Custom }, OrganizationUserType.Admin],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Custom }, OrganizationUserType.Custom],
|
||||
[new CurrentContextOrganization { Type = OrganizationUserType.Custom }, OrganizationUserType.User],
|
||||
};
|
||||
|
||||
[Theory, BitMemberAutoData(nameof(UnauthorizedRoleCombinations))]
|
||||
public async Task AuthorizeMemberAsync_InvalidRoles_TargetUserNotProvider_Unauthorized(
|
||||
CurrentContextOrganization currentContextOrganization,
|
||||
OrganizationUserType targetOrganizationUserType,
|
||||
SutProvider<RecoverAccountAuthorizationHandler> sutProvider,
|
||||
[OrganizationUser] OrganizationUser targetOrganizationUser,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
// Arrange
|
||||
targetOrganizationUser.Type = targetOrganizationUserType;
|
||||
currentContextOrganization.Id = targetOrganizationUser.OrganizationId;
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
[new RecoverAccountAuthorizationRequirement()],
|
||||
claimsPrincipal,
|
||||
targetOrganizationUser);
|
||||
|
||||
MockOrganizationClaims(sutProvider, claimsPrincipal, targetOrganizationUser, currentContextOrganization);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
AssertFailed(context, RecoverAccountAuthorizationHandler.FailureReason);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_TargetUserIdIsNull_DoesNotBlock(
|
||||
SutProvider<RecoverAccountAuthorizationHandler> sutProvider,
|
||||
OrganizationUser targetOrganizationUser,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
// Arrange
|
||||
targetOrganizationUser.UserId = null;
|
||||
MockCurrentUserIsOwner(sutProvider, claimsPrincipal, targetOrganizationUser);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
[new RecoverAccountAuthorizationRequirement()],
|
||||
claimsPrincipal,
|
||||
targetOrganizationUser);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded);
|
||||
// This should shortcut the provider escalation check
|
||||
await sutProvider.GetDependency<IProviderUserRepository>().DidNotReceiveWithAnyArgs()
|
||||
.GetManyByUserAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_CurrentUserIsMemberOfAllTargetUserProviders_DoesNotBlock(
|
||||
SutProvider<RecoverAccountAuthorizationHandler> sutProvider,
|
||||
[OrganizationUser] OrganizationUser targetOrganizationUser,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
Guid providerId1,
|
||||
Guid providerId2)
|
||||
{
|
||||
// Arrange
|
||||
var targetUserProviders = new List<ProviderUser>
|
||||
{
|
||||
new() { ProviderId = providerId1, UserId = targetOrganizationUser.UserId },
|
||||
new() { ProviderId = providerId2, UserId = targetOrganizationUser.UserId }
|
||||
};
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
[new RecoverAccountAuthorizationRequirement()],
|
||||
claimsPrincipal,
|
||||
targetOrganizationUser);
|
||||
|
||||
MockCurrentUserIsProvider(sutProvider, claimsPrincipal, targetOrganizationUser);
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(targetOrganizationUser.UserId!.Value)
|
||||
.Returns(targetUserProviders);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ProviderUser(providerId1)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ProviderUser(providerId2)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleRequirementAsync_CurrentUserMissingProviderMembership_Blocks(
|
||||
SutProvider<RecoverAccountAuthorizationHandler> sutProvider,
|
||||
[OrganizationUser] OrganizationUser targetOrganizationUser,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
Guid providerId1,
|
||||
Guid providerId2)
|
||||
{
|
||||
// Arrange
|
||||
var targetUserProviders = new List<ProviderUser>
|
||||
{
|
||||
new() { ProviderId = providerId1, UserId = targetOrganizationUser.UserId },
|
||||
new() { ProviderId = providerId2, UserId = targetOrganizationUser.UserId }
|
||||
};
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
[new RecoverAccountAuthorizationRequirement()],
|
||||
claimsPrincipal,
|
||||
targetOrganizationUser);
|
||||
|
||||
MockCurrentUserIsOwner(sutProvider, claimsPrincipal, targetOrganizationUser);
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(targetOrganizationUser.UserId!.Value)
|
||||
.Returns(targetUserProviders);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ProviderUser(providerId1)
|
||||
.Returns(true);
|
||||
|
||||
// Not a member of this provider
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ProviderUser(providerId2)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
// Assert
|
||||
AssertFailed(context, RecoverAccountAuthorizationHandler.ProviderFailureReason);
|
||||
}
|
||||
|
||||
private static void MockOrganizationClaims(SutProvider<RecoverAccountAuthorizationHandler> sutProvider,
|
||||
ClaimsPrincipal currentUser, OrganizationUser targetOrganizationUser,
|
||||
CurrentContextOrganization? currentContextOrganization)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationContext>()
|
||||
.GetOrganizationClaims(currentUser, targetOrganizationUser.OrganizationId)
|
||||
.Returns(currentContextOrganization);
|
||||
}
|
||||
|
||||
private static void MockCurrentUserIsProvider(SutProvider<RecoverAccountAuthorizationHandler> sutProvider,
|
||||
ClaimsPrincipal currentUser, OrganizationUser targetOrganizationUser)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationContext>()
|
||||
.IsProviderUserForOrganization(currentUser, targetOrganizationUser.OrganizationId)
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
private static void MockCurrentUserIsOwner(SutProvider<RecoverAccountAuthorizationHandler> sutProvider,
|
||||
ClaimsPrincipal currentUser, OrganizationUser targetOrganizationUser)
|
||||
{
|
||||
var currentContextOrganization = new CurrentContextOrganization
|
||||
{
|
||||
Id = targetOrganizationUser.OrganizationId,
|
||||
Type = OrganizationUserType.Owner
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationContext>()
|
||||
.GetOrganizationClaims(currentUser, targetOrganizationUser.OrganizationId)
|
||||
.Returns(currentContextOrganization);
|
||||
}
|
||||
|
||||
private static void AssertFailed(AuthorizationHandlerContext context, string expectedMessage)
|
||||
{
|
||||
Assert.True(context.HasFailed);
|
||||
var failureReason = Assert.Single(context.FailureReasons);
|
||||
Assert.Equal(expectedMessage, failureReason.Message);
|
||||
}
|
||||
}
|
||||
@@ -133,6 +133,29 @@ public class OrganizationIntegrationControllerTests
|
||||
.DeleteAsync(organizationIntegration);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostDeleteAsync_AllParamsProvided_Succeeds(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
|
||||
await sutProvider.Sut.PostDeleteAsync(organizationId, organizationIntegration.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.GetByIdAsync(organizationIntegration.Id);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.DeleteAsync(organizationIntegration);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
|
||||
@@ -51,6 +51,36 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
.DeleteAsync(organizationIntegrationConfiguration);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostDeleteAsync_AllParamsProvided_Succeeds(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
|
||||
await sutProvider.Sut.PostDeleteAsync(organizationId, organizationIntegration.Id, organizationIntegrationConfiguration.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.GetByIdAsync(organizationIntegration.Id);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.GetByIdAsync(organizationIntegrationConfiguration.Id);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.DeleteAsync(organizationIntegrationConfiguration);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
@@ -199,27 +229,6 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
.GetManyByIntegrationAsync(organizationIntegration.Id);
|
||||
}
|
||||
|
||||
// [Theory, BitAutoData]
|
||||
// public async Task GetAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
|
||||
// SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
// Guid organizationId,
|
||||
// OrganizationIntegration organizationIntegration)
|
||||
// {
|
||||
// organizationIntegration.OrganizationId = organizationId;
|
||||
// sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
// sutProvider.GetDependency<ICurrentContext>()
|
||||
// .OrganizationOwner(organizationId)
|
||||
// .Returns(true);
|
||||
// sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
// .GetByIdAsync(Arg.Any<Guid>())
|
||||
// .Returns(organizationIntegration);
|
||||
// sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
// .GetByIdAsync(Arg.Any<Guid>())
|
||||
// .ReturnsNull();
|
||||
//
|
||||
// await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.Empty, Guid.Empty));
|
||||
// }
|
||||
//
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAsync_IntegrationDoesNotExist_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
@@ -293,15 +302,16 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
|
||||
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse);
|
||||
Assert.Equal(expected.Id, createResponse.Id);
|
||||
Assert.Equal(expected.Configuration, createResponse.Configuration);
|
||||
Assert.Equal(expected.EventType, createResponse.EventType);
|
||||
Assert.Equal(expected.Filters, createResponse.Filters);
|
||||
Assert.Equal(expected.Template, createResponse.Template);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -331,15 +341,16 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
|
||||
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse);
|
||||
Assert.Equal(expected.Id, createResponse.Id);
|
||||
Assert.Equal(expected.Configuration, createResponse.Configuration);
|
||||
Assert.Equal(expected.EventType, createResponse.EventType);
|
||||
Assert.Equal(expected.Filters, createResponse.Filters);
|
||||
Assert.Equal(expected.Template, createResponse.Template);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -369,15 +380,16 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
|
||||
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse);
|
||||
Assert.Equal(expected.Id, createResponse.Id);
|
||||
Assert.Equal(expected.Configuration, createResponse.Configuration);
|
||||
Assert.Equal(expected.EventType, createResponse.EventType);
|
||||
Assert.Equal(expected.Filters, createResponse.Filters);
|
||||
Assert.Equal(expected.Template, createResponse.Template);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -575,7 +587,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.UpdateAsync(
|
||||
var updateResponse = await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
organizationIntegrationConfiguration.Id,
|
||||
@@ -583,11 +595,12 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse);
|
||||
Assert.Equal(expected.Id, updateResponse.Id);
|
||||
Assert.Equal(expected.Configuration, updateResponse.Configuration);
|
||||
Assert.Equal(expected.EventType, updateResponse.EventType);
|
||||
Assert.Equal(expected.Filters, updateResponse.Filters);
|
||||
Assert.Equal(expected.Template, updateResponse.Template);
|
||||
}
|
||||
|
||||
|
||||
@@ -619,7 +632,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.UpdateAsync(
|
||||
var updateResponse = await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
organizationIntegrationConfiguration.Id,
|
||||
@@ -627,11 +640,12 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse);
|
||||
Assert.Equal(expected.Id, updateResponse.Id);
|
||||
Assert.Equal(expected.Configuration, updateResponse.Configuration);
|
||||
Assert.Equal(expected.EventType, updateResponse.EventType);
|
||||
Assert.Equal(expected.Filters, updateResponse.Filters);
|
||||
Assert.Equal(expected.Template, updateResponse.Template);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -662,7 +676,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.UpdateAsync(
|
||||
var updateResponse = await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
organizationIntegrationConfiguration.Id,
|
||||
@@ -670,11 +684,12 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse);
|
||||
Assert.Equal(expected.Id, updateResponse.Id);
|
||||
Assert.Equal(expected.Configuration, updateResponse.Configuration);
|
||||
Assert.Equal(expected.EventType, updateResponse.EventType);
|
||||
Assert.Equal(expected.Filters, updateResponse.Filters);
|
||||
Assert.Equal(expected.Template, updateResponse.Template);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
@@ -16,6 +19,7 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
@@ -30,6 +34,7 @@ using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@@ -440,4 +445,153 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
Assert.Equal("Master Password reset is required, but not provided.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutResetPassword_WithFeatureFlagDisabled_CallsLegacyPath(
|
||||
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountRecoveryCommand).Returns(false);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(orgId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().AdminResetPasswordAsync(Arg.Any<OrganizationUserType>(), orgId, orgUserId, model.NewMasterPasswordHash, model.Key)
|
||||
.Returns(Microsoft.AspNetCore.Identity.IdentityResult.Success);
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<Ok>(result);
|
||||
await sutProvider.GetDependency<IUserService>().Received(1)
|
||||
.AdminResetPasswordAsync(OrganizationUserType.Owner, orgId, orgUserId, model.NewMasterPasswordHash, model.Key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutResetPassword_WithFeatureFlagDisabled_WhenOrgUserTypeIsNull_ReturnsNotFound(
|
||||
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountRecoveryCommand).Returns(false);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(orgId).Returns(false);
|
||||
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(new List<CurrentContextOrganization>());
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<NotFound>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutResetPassword_WithFeatureFlagDisabled_WhenAdminResetPasswordFails_ReturnsBadRequest(
|
||||
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountRecoveryCommand).Returns(false);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(orgId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().AdminResetPasswordAsync(Arg.Any<OrganizationUserType>(), orgId, orgUserId, model.NewMasterPasswordHash, model.Key)
|
||||
.Returns(Microsoft.AspNetCore.Identity.IdentityResult.Failed(new Microsoft.AspNetCore.Identity.IdentityError { Description = "Error 1" }));
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<BadRequest<ModelStateDictionary>>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutResetPassword_WithFeatureFlagEnabled_WhenOrganizationUserNotFound_ReturnsNotFound(
|
||||
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountRecoveryCommand).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns((OrganizationUser)null);
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<NotFound>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutResetPassword_WithFeatureFlagEnabled_WhenOrganizationIdMismatch_ReturnsNotFound(
|
||||
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
organizationUser.OrganizationId = Guid.NewGuid();
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountRecoveryCommand).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<NotFound>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutResetPassword_WithFeatureFlagEnabled_WhenAuthorizationFails_ReturnsBadRequest(
|
||||
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
organizationUser.OrganizationId = orgId;
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountRecoveryCommand).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(),
|
||||
organizationUser,
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))
|
||||
.Returns(AuthorizationResult.Failed());
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<BadRequest<ErrorResponseModel>>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutResetPassword_WithFeatureFlagEnabled_WhenRecoverAccountSucceeds_ReturnsOk(
|
||||
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
organizationUser.OrganizationId = orgId;
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountRecoveryCommand).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(),
|
||||
organizationUser,
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))
|
||||
.Returns(AuthorizationResult.Success());
|
||||
sutProvider.GetDependency<IAdminRecoverAccountCommand>()
|
||||
.RecoverAccountAsync(orgId, organizationUser, model.NewMasterPasswordHash, model.Key)
|
||||
.Returns(Microsoft.AspNetCore.Identity.IdentityResult.Success);
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<Ok>(result);
|
||||
await sutProvider.GetDependency<IAdminRecoverAccountCommand>().Received(1)
|
||||
.RecoverAccountAsync(orgId, organizationUser, model.NewMasterPasswordHash, model.Key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutResetPassword_WithFeatureFlagEnabled_WhenRecoverAccountFails_ReturnsBadRequest(
|
||||
Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
organizationUser.OrganizationId = orgId;
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.AccountRecoveryCommand).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(
|
||||
Arg.Any<ClaimsPrincipal>(),
|
||||
organizationUser,
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))
|
||||
.Returns(AuthorizationResult.Success());
|
||||
sutProvider.GetDependency<IAdminRecoverAccountCommand>()
|
||||
.RecoverAccountAsync(orgId, organizationUser, model.NewMasterPasswordHash, model.Key)
|
||||
.Returns(Microsoft.AspNetCore.Identity.IdentityResult.Failed(new Microsoft.AspNetCore.Identity.IdentityError { Description = "Error message" }));
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<BadRequest<ModelStateDictionary>>(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
#nullable enable
|
||||
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
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;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@@ -16,98 +22,350 @@ namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
[SutProviderCustomize]
|
||||
public class SlackIntegrationControllerTests
|
||||
{
|
||||
private const string _slackToken = "xoxb-test-token";
|
||||
private const string _validSlackCode = "A_test_code";
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_AllParamsProvided_Succeeds(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
public async Task CreateAsync_AllParamsProvided_Succeeds(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
var token = "xoxb-test-token";
|
||||
integration.Type = IntegrationType.Slack;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(token);
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
.Returns(_slackToken);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
||||
.Returns(callInfo => callInfo.Arg<OrganizationIntegration>());
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, "A_test_code");
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString());
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>());
|
||||
.UpsertAsync(Arg.Any<OrganizationIntegration>());
|
||||
Assert.IsType<CreatedResult>(requestAction);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Slack;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(organizationId, string.Empty));
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.CreateAsync(string.Empty, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_SlackServiceReturnsEmpty_ThrowsBadRequest(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
public async Task CreateAsync_CallbackUrlIsEmpty_ThrowsBadRequest(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Slack;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns((string?)null);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_SlackServiceReturnsEmpty_ThrowsBadRequest(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Slack;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
.Returns(string.Empty);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(organizationId, "A_test_code"));
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
public async Task CreateAsync_StateEmpty_ThrowsNotFound(
|
||||
SutProvider<SlackIntegrationController> sutProvider)
|
||||
{
|
||||
var token = "xoxb-test-token";
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(token);
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
.Returns(_slackToken);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(organizationId, "A_test_code"));
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, string.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_Success(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
public async Task CreateAsync_StateExpired_ThrowsNotFound(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
var expectedUrl = $"https://localhost/{organizationId}";
|
||||
var timeProvider = new FakeTimeProvider(new DateTime(2024, 4, 3, 2, 1, 0, DateTimeKind.Utc));
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
.Returns(_slackToken);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);
|
||||
timeProvider.Advance(TimeSpan.FromMinutes(30));
|
||||
|
||||
sutProvider.SetDependency<TimeProvider>(timeProvider);
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasNonexistentIntegration_ThrowsNotFound(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
.Returns(_slackToken);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration,
|
||||
OrganizationIntegration wrongOrgIntegration)
|
||||
{
|
||||
wrongOrgIntegration.Id = integration.Id;
|
||||
wrongOrgIntegration.Type = IntegrationType.Slack;
|
||||
wrongOrgIntegration.Configuration = null;
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(expectedUrl);
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
.Returns(_slackToken);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(wrongOrgIntegration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasNonEmptyIntegration_ThrowsNotFound(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Slack;
|
||||
integration.Configuration = "{}";
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
.Returns(_slackToken);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasNonSlackIntegration_ThrowsNotFound(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Hec;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())
|
||||
.Returns(_slackToken);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_Success(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Configuration = null;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(integration.OrganizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(integration.OrganizationId)
|
||||
.Returns([]);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
||||
.Returns(integration);
|
||||
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
|
||||
|
||||
var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
var requestAction = await sutProvider.Sut.RedirectAsync(integration.OrganizationId);
|
||||
|
||||
Assert.IsType<RedirectResult>(requestAction);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>());
|
||||
sutProvider.GetDependency<ISlackService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_IntegrationAlreadyExistsWithNullConfig_Success(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.OrganizationId = organizationId;
|
||||
integration.Configuration = null;
|
||||
integration.Type = IntegrationType.Slack;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.HttpContext.Request.Scheme
|
||||
.Returns("https");
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(organizationId)
|
||||
.Returns([integration]);
|
||||
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
|
||||
|
||||
var requestAction = await sutProvider.Sut.RedirectAsync(organizationId);
|
||||
|
||||
var redirectResult = Assert.IsType<RedirectResult>(requestAction);
|
||||
Assert.Equal(expectedUrl, redirectResult.Url);
|
||||
var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
Assert.IsType<RedirectResult>(requestAction);
|
||||
sutProvider.GetDependency<ISlackService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_SlackServiceReturnsEmpty_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.OrganizationId = organizationId;
|
||||
integration.Configuration = "{}";
|
||||
integration.Type = IntegrationType.Slack;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(string.Empty);
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(organizationId)
|
||||
.Returns([integration]);
|
||||
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_CallbackUrlReturnsEmpty_ThrowsBadRequest(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns((string?)null);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.HttpContext.Request.Scheme
|
||||
.Returns("https");
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_SlackServiceReturnsEmpty_ThrowsNotFound(
|
||||
SutProvider<SlackIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.OrganizationId = organizationId;
|
||||
integration.Configuration = null;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(organizationId)
|
||||
.Returns([]);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
||||
.Returns(integration);
|
||||
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(string.Empty);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
@@ -116,14 +374,9 @@ public class SlackIntegrationControllerTests
|
||||
public async Task RedirectAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(string.Empty);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.HttpContext.Request.Scheme
|
||||
.Returns("https");
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
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;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.Bot.Builder;
|
||||
using Microsoft.Bot.Builder.Integration.AspNet.Core;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(TeamsIntegrationController))]
|
||||
[SutProviderCustomize]
|
||||
public class TeamsIntegrationControllerTests
|
||||
{
|
||||
private const string _teamsToken = "test-token";
|
||||
private const string _validTeamsCode = "A_test_code";
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_AllParamsProvided_Succeeds(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.GetJoinedTeamsAsync(_teamsToken)
|
||||
.Returns([
|
||||
new TeamInfo() { DisplayName = "Test Team", Id = Guid.NewGuid().ToString(), TenantId = Guid.NewGuid().ToString() }
|
||||
]);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString());
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.UpsertAsync(Arg.Any<OrganizationIntegration>());
|
||||
Assert.IsType<CreatedResult>(requestAction);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_CallbackUrlIsEmpty_ThrowsBadRequest(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns((string?)null);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.CreateAsync(string.Empty, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_NoTeamsFound_ThrowsBadRequest(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.GetJoinedTeamsAsync(_teamsToken)
|
||||
.Returns([]);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_TeamsServiceReturnsEmptyToken_ThrowsBadRequest(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(string.Empty);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateEmpty_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, string.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateExpired_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(new DateTime(2024, 4, 3, 2, 1, 0, DateTimeKind.Utc));
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);
|
||||
timeProvider.Advance(TimeSpan.FromMinutes(30));
|
||||
|
||||
sutProvider.SetDependency<TimeProvider>(timeProvider);
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasNonexistentIntegration_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration,
|
||||
OrganizationIntegration wrongOrgIntegration)
|
||||
{
|
||||
wrongOrgIntegration.Id = integration.Id;
|
||||
wrongOrgIntegration.Type = IntegrationType.Teams;
|
||||
wrongOrgIntegration.Configuration = null;
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(wrongOrgIntegration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasNonEmptyIntegration_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Teams;
|
||||
integration.Configuration = "{}";
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_StateHasNonTeamsIntegration_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Type = IntegrationType.Hec;
|
||||
integration.Configuration = null;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns("https://localhost");
|
||||
sutProvider.GetDependency<ITeamsService>()
|
||||
.ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())
|
||||
.Returns(_teamsToken);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(integration.Id)
|
||||
.Returns(integration);
|
||||
|
||||
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_Success(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.Configuration = null;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(integration.OrganizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(integration.OrganizationId)
|
||||
.Returns([]);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
||||
.Returns(integration);
|
||||
sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
|
||||
|
||||
var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
var requestAction = await sutProvider.Sut.RedirectAsync(integration.OrganizationId);
|
||||
|
||||
Assert.IsType<RedirectResult>(requestAction);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>());
|
||||
sutProvider.GetDependency<ITeamsService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_IntegrationAlreadyExistsWithNullConfig_Success(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.OrganizationId = organizationId;
|
||||
integration.Configuration = null;
|
||||
integration.Type = IntegrationType.Teams;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(organizationId)
|
||||
.Returns([integration]);
|
||||
sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
|
||||
|
||||
var requestAction = await sutProvider.Sut.RedirectAsync(organizationId);
|
||||
|
||||
var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
|
||||
|
||||
Assert.IsType<RedirectResult>(requestAction);
|
||||
sutProvider.GetDependency<ITeamsService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_CallbackUrlIsEmpty_ThrowsBadRequest(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.OrganizationId = organizationId;
|
||||
integration.Configuration = null;
|
||||
integration.Type = IntegrationType.Teams;
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns((string?)null);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(organizationId)
|
||||
.Returns([integration]);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.OrganizationId = organizationId;
|
||||
integration.Configuration = "{}";
|
||||
integration.Type = IntegrationType.Teams;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(organizationId)
|
||||
.Returns([integration]);
|
||||
sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_TeamsServiceReturnsEmpty_ThrowsNotFound(
|
||||
SutProvider<TeamsIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration integration)
|
||||
{
|
||||
integration.OrganizationId = organizationId;
|
||||
integration.Configuration = null;
|
||||
var expectedUrl = "https://localhost/";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.Sut.Url
|
||||
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
|
||||
.Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetManyByOrganizationAsync(organizationId)
|
||||
.Returns([]);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
||||
.Returns(integration);
|
||||
sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(string.Empty);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<TeamsIntegrationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task IncomingPostAsync_ForwardsToBot(SutProvider<TeamsIntegrationController> sutProvider)
|
||||
{
|
||||
var adapter = sutProvider.GetDependency<IBotFrameworkHttpAdapter>();
|
||||
var bot = sutProvider.GetDependency<IBot>();
|
||||
|
||||
await sutProvider.Sut.IncomingPostAsync();
|
||||
await adapter.Received(1).ProcessAsync(Arg.Any<HttpRequest>(), Arg.Any<HttpResponse>(), bot);
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
[Theory]
|
||||
[InlineData(data: "")]
|
||||
[InlineData(data: " ")]
|
||||
public void IsValidForType_EmptyNonNullHecConfiguration_ReturnsFalse(string? config)
|
||||
public void IsValidForType_EmptyNonNullConfiguration_ReturnsFalse(string? config)
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
@@ -48,10 +48,12 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
};
|
||||
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Datadog));
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Teams));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForType_NullHecConfiguration_ReturnsTrue()
|
||||
public void IsValidForType_NullConfiguration_ReturnsTrue()
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
@@ -60,32 +62,8 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
};
|
||||
|
||||
Assert.True(condition: model.IsValidForType(IntegrationType.Hec));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(data: "")]
|
||||
[InlineData(data: " ")]
|
||||
public void IsValidForType_EmptyNonNullDatadogConfiguration_ReturnsFalse(string? config)
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = config,
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Datadog));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForType_NullDatadogConfiguration_ReturnsTrue()
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = null,
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
Assert.True(condition: model.IsValidForType(IntegrationType.Datadog));
|
||||
Assert.True(condition: model.IsValidForType(IntegrationType.Teams));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -107,6 +85,8 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Datadog));
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Teams));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -121,6 +101,8 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Datadog));
|
||||
Assert.False(condition: model.IsValidForType(IntegrationType.Teams));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,47 @@
|
||||
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.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationIntegrationRequestModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToOrganizationIntegration_CreatesNewOrganizationIntegration()
|
||||
{
|
||||
var model = new OrganizationIntegrationRequestModel
|
||||
{
|
||||
Type = IntegrationType.Hec,
|
||||
Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri("http://localhost"), Scheme: "Bearer", Token: "Token"))
|
||||
};
|
||||
|
||||
var organizationId = Guid.NewGuid();
|
||||
var organizationIntegration = model.ToOrganizationIntegration(organizationId);
|
||||
|
||||
Assert.Equal(organizationIntegration.Type, model.Type);
|
||||
Assert.Equal(organizationIntegration.Configuration, model.Configuration);
|
||||
Assert.Equal(organizationIntegration.OrganizationId, organizationId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ToOrganizationIntegration_UpdatesExistingOrganizationIntegration(OrganizationIntegration integration)
|
||||
{
|
||||
var model = new OrganizationIntegrationRequestModel
|
||||
{
|
||||
Type = IntegrationType.Hec,
|
||||
Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri("http://localhost"), Scheme: "Bearer", Token: "Token"))
|
||||
};
|
||||
|
||||
var organizationIntegration = model.ToOrganizationIntegration(integration);
|
||||
|
||||
Assert.Equal(organizationIntegration.Configuration, model.Configuration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CloudBillingSync_ReturnsNotYetSupportedError()
|
||||
{
|
||||
@@ -57,6 +90,22 @@ public class OrganizationIntegrationRequestModelTests
|
||||
Assert.Contains("cannot be created directly", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Teams_ReturnsCannotBeCreatedDirectlyError()
|
||||
{
|
||||
var model = new OrganizationIntegrationRequestModel
|
||||
{
|
||||
Type = IntegrationType.Teams,
|
||||
Configuration = null
|
||||
};
|
||||
|
||||
var results = model.Validate(new ValidationContext(model)).ToList();
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Contains(nameof(model.Type), results[0].MemberNames);
|
||||
Assert.Contains("cannot be created directly", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Webhook_WithNullConfiguration_ReturnsNoErrors()
|
||||
{
|
||||
|
||||
@@ -24,11 +24,11 @@ public class SavePolicyRequestTests
|
||||
currentContext.OrganizationOwner(organizationId).Returns(true);
|
||||
|
||||
var testData = new Dictionary<string, object> { { "test", "value" } };
|
||||
var policyType = PolicyType.TwoFactorAuthentication;
|
||||
var model = new SavePolicyRequest
|
||||
{
|
||||
Policy = new PolicyRequestModel
|
||||
{
|
||||
Type = PolicyType.TwoFactorAuthentication,
|
||||
Enabled = true,
|
||||
Data = testData
|
||||
},
|
||||
@@ -36,7 +36,7 @@ public class SavePolicyRequestTests
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(PolicyType.TwoFactorAuthentication, result.PolicyUpdate.Type);
|
||||
@@ -54,7 +54,7 @@ public class SavePolicyRequestTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ToSavePolicyModelAsync_WithNullData_HandlesCorrectly(
|
||||
public async Task ToSavePolicyModelAsync_WithEmptyData_HandlesCorrectly(
|
||||
Guid organizationId,
|
||||
Guid userId)
|
||||
{
|
||||
@@ -63,19 +63,17 @@ public class SavePolicyRequestTests
|
||||
currentContext.UserId.Returns(userId);
|
||||
currentContext.OrganizationOwner(organizationId).Returns(false);
|
||||
|
||||
var policyType = PolicyType.SingleOrg;
|
||||
var model = new SavePolicyRequest
|
||||
{
|
||||
Policy = new PolicyRequestModel
|
||||
{
|
||||
Type = PolicyType.SingleOrg,
|
||||
Enabled = false,
|
||||
Data = null
|
||||
},
|
||||
Metadata = null
|
||||
Enabled = false
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.PolicyUpdate.Data);
|
||||
@@ -95,19 +93,17 @@ public class SavePolicyRequestTests
|
||||
currentContext.UserId.Returns(userId);
|
||||
currentContext.OrganizationOwner(organizationId).Returns(true);
|
||||
|
||||
var policyType = PolicyType.SingleOrg;
|
||||
var model = new SavePolicyRequest
|
||||
{
|
||||
Policy = new PolicyRequestModel
|
||||
{
|
||||
Type = PolicyType.SingleOrg,
|
||||
Enabled = false,
|
||||
Data = null
|
||||
},
|
||||
Metadata = null
|
||||
Enabled = false
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.PolicyUpdate.Data);
|
||||
@@ -128,13 +124,12 @@ public class SavePolicyRequestTests
|
||||
currentContext.UserId.Returns(userId);
|
||||
currentContext.OrganizationOwner(organizationId).Returns(true);
|
||||
|
||||
var policyType = PolicyType.OrganizationDataOwnership;
|
||||
var model = new SavePolicyRequest
|
||||
{
|
||||
Policy = new PolicyRequestModel
|
||||
{
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true,
|
||||
Data = null
|
||||
Enabled = true
|
||||
},
|
||||
Metadata = new Dictionary<string, object>
|
||||
{
|
||||
@@ -143,7 +138,7 @@ public class SavePolicyRequestTests
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<OrganizationModelOwnershipPolicyModel>(result.Metadata);
|
||||
@@ -152,7 +147,7 @@ public class SavePolicyRequestTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ToSavePolicyModelAsync_OrganizationDataOwnership_WithNullMetadata_ReturnsEmptyMetadata(
|
||||
public async Task ToSavePolicyModelAsync_OrganizationDataOwnership_WithEmptyMetadata_ReturnsEmptyMetadata(
|
||||
Guid organizationId,
|
||||
Guid userId)
|
||||
{
|
||||
@@ -161,19 +156,17 @@ public class SavePolicyRequestTests
|
||||
currentContext.UserId.Returns(userId);
|
||||
currentContext.OrganizationOwner(organizationId).Returns(true);
|
||||
|
||||
var policyType = PolicyType.OrganizationDataOwnership;
|
||||
var model = new SavePolicyRequest
|
||||
{
|
||||
Policy = new PolicyRequestModel
|
||||
{
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true,
|
||||
Data = null
|
||||
},
|
||||
Metadata = null
|
||||
Enabled = true
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
@@ -200,12 +193,11 @@ public class SavePolicyRequestTests
|
||||
currentContext.UserId.Returns(userId);
|
||||
currentContext.OrganizationOwner(organizationId).Returns(true);
|
||||
|
||||
|
||||
var policyType = PolicyType.ResetPassword;
|
||||
var model = new SavePolicyRequest
|
||||
{
|
||||
Policy = new PolicyRequestModel
|
||||
{
|
||||
Type = PolicyType.ResetPassword,
|
||||
Enabled = true,
|
||||
Data = _complexData
|
||||
},
|
||||
@@ -213,7 +205,7 @@ public class SavePolicyRequestTests
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
|
||||
|
||||
// Assert
|
||||
var deserializedData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(result.PolicyUpdate.Data);
|
||||
@@ -241,13 +233,12 @@ public class SavePolicyRequestTests
|
||||
currentContext.UserId.Returns(userId);
|
||||
currentContext.OrganizationOwner(organizationId).Returns(true);
|
||||
|
||||
var policyType = PolicyType.MaximumVaultTimeout;
|
||||
var model = new SavePolicyRequest
|
||||
{
|
||||
Policy = new PolicyRequestModel
|
||||
{
|
||||
Type = PolicyType.MaximumVaultTimeout,
|
||||
Enabled = true,
|
||||
Data = null
|
||||
Enabled = true
|
||||
},
|
||||
Metadata = new Dictionary<string, object>
|
||||
{
|
||||
@@ -256,7 +247,7 @@ public class SavePolicyRequestTests
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
@@ -274,20 +265,18 @@ public class SavePolicyRequestTests
|
||||
currentContext.OrganizationOwner(organizationId).Returns(true);
|
||||
|
||||
var errorDictionary = BuildErrorDictionary();
|
||||
|
||||
var policyType = PolicyType.OrganizationDataOwnership;
|
||||
var model = new SavePolicyRequest
|
||||
{
|
||||
Policy = new PolicyRequestModel
|
||||
{
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true,
|
||||
Data = null
|
||||
Enabled = true
|
||||
},
|
||||
Metadata = errorDictionary
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
|
||||
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
#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.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class OrganizationIntegrationResponseModelTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void Status_CloudBillingSync_AlwaysNotApplicable(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.CloudBillingSync;
|
||||
oi.Configuration = null;
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);
|
||||
|
||||
model.Configuration = "{}";
|
||||
Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Scim_AlwaysNotApplicable(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Scim;
|
||||
oi.Configuration = null;
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);
|
||||
|
||||
model.Configuration = "{}";
|
||||
Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Slack_NullConfig_ReturnsInitiated(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Slack;
|
||||
oi.Configuration = null;
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.Initiated, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Slack_WithConfig_ReturnsCompleted(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Slack;
|
||||
oi.Configuration = "{}";
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Teams_NullConfig_ReturnsInitiated(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Teams;
|
||||
oi.Configuration = null;
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.Initiated, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Teams_WithTenantAndTeamsConfig_ReturnsInProgress(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Teams;
|
||||
oi.Configuration = JsonSerializer.Serialize(new TeamsIntegration(
|
||||
TenantId: "tenant", Teams: [new TeamInfo() { DisplayName = "Team", Id = "TeamId", TenantId = "tenant" }]
|
||||
));
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.InProgress, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Teams_WithCompletedConfig_ReturnsCompleted(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Teams;
|
||||
oi.Configuration = JsonSerializer.Serialize(new TeamsIntegration(
|
||||
TenantId: "tenant",
|
||||
Teams: [new TeamInfo() { DisplayName = "Team", Id = "TeamId", TenantId = "tenant" }],
|
||||
ServiceUrl: new Uri("https://example.com"),
|
||||
ChannelId: "channellId"
|
||||
));
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Webhook_AlwaysCompleted(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Webhook;
|
||||
oi.Configuration = null;
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
|
||||
|
||||
model.Configuration = "{}";
|
||||
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Hec_NullConfig_ReturnsInvalid(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Hec;
|
||||
oi.Configuration = null;
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.Invalid, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Hec_WithConfig_ReturnsCompleted(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Hec;
|
||||
oi.Configuration = "{}";
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Datadog_NullConfig_ReturnsInvalid(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Datadog;
|
||||
oi.Configuration = null;
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.Invalid, model.Status);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void Status_Datadog_WithConfig_ReturnsCompleted(OrganizationIntegration oi)
|
||||
{
|
||||
oi.Type = IntegrationType.Datadog;
|
||||
oi.Configuration = "{}";
|
||||
|
||||
var model = new OrganizationIntegrationResponseModel(oi);
|
||||
|
||||
Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Response;
|
||||
|
||||
public class ProfileOrganizationResponseModelTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void Constructor_ShouldPopulatePropertiesCorrectly(Organization organization)
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var organizationUserId = Guid.NewGuid();
|
||||
var providerId = Guid.NewGuid();
|
||||
var organizationIdsClaimingUser = new[] { organization.Id };
|
||||
|
||||
var organizationDetails = new OrganizationUserOrganizationDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = userId,
|
||||
OrganizationUserId = organizationUserId,
|
||||
Name = organization.Name,
|
||||
Enabled = organization.Enabled,
|
||||
Identifier = organization.Identifier,
|
||||
PlanType = organization.PlanType,
|
||||
UsePolicies = organization.UsePolicies,
|
||||
UseSso = organization.UseSso,
|
||||
UseKeyConnector = organization.UseKeyConnector,
|
||||
UseScim = organization.UseScim,
|
||||
UseGroups = organization.UseGroups,
|
||||
UseDirectory = organization.UseDirectory,
|
||||
UseEvents = organization.UseEvents,
|
||||
UseTotp = organization.UseTotp,
|
||||
Use2fa = organization.Use2fa,
|
||||
UseApi = organization.UseApi,
|
||||
UseResetPassword = organization.UseResetPassword,
|
||||
UseSecretsManager = organization.UseSecretsManager,
|
||||
UsePasswordManager = organization.UsePasswordManager,
|
||||
UsersGetPremium = organization.UsersGetPremium,
|
||||
UseCustomPermissions = organization.UseCustomPermissions,
|
||||
UseRiskInsights = organization.UseRiskInsights,
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains,
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies,
|
||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation,
|
||||
SelfHost = organization.SelfHost,
|
||||
Seats = organization.Seats,
|
||||
MaxCollections = organization.MaxCollections,
|
||||
MaxStorageGb = organization.MaxStorageGb,
|
||||
Key = "organization-key",
|
||||
PublicKey = "public-key",
|
||||
PrivateKey = "private-key",
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
||||
LimitItemDeletion = organization.LimitItemDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems,
|
||||
ProviderId = providerId,
|
||||
ProviderName = "Test Provider",
|
||||
ProviderType = ProviderType.Msp,
|
||||
SsoEnabled = true,
|
||||
SsoConfig = new SsoConfigurationData
|
||||
{
|
||||
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
||||
KeyConnectorUrl = "https://keyconnector.example.com"
|
||||
}.Serialize(),
|
||||
SsoExternalId = "external-sso-id",
|
||||
Permissions = CoreHelpers.ClassToJsonData(new Core.Models.Data.Permissions { ManageUsers = true }),
|
||||
ResetPasswordKey = "reset-password-key",
|
||||
FamilySponsorshipFriendlyName = "Family Sponsorship",
|
||||
FamilySponsorshipLastSyncDate = DateTime.UtcNow.AddDays(-1),
|
||||
FamilySponsorshipToDelete = false,
|
||||
FamilySponsorshipValidUntil = DateTime.UtcNow.AddYears(1),
|
||||
IsAdminInitiated = true,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
Type = OrganizationUserType.Owner,
|
||||
AccessSecretsManager = true,
|
||||
SmSeats = 5,
|
||||
SmServiceAccounts = 10
|
||||
};
|
||||
|
||||
var result = new ProfileOrganizationResponseModel(organizationDetails, organizationIdsClaimingUser);
|
||||
|
||||
Assert.Equal("profileOrganization", result.Object);
|
||||
Assert.Equal(organization.Id, result.Id);
|
||||
Assert.Equal(userId, result.UserId);
|
||||
Assert.Equal(organization.Name, result.Name);
|
||||
Assert.Equal(organization.Enabled, result.Enabled);
|
||||
Assert.Equal(organization.Identifier, result.Identifier);
|
||||
Assert.Equal(organization.PlanType.GetProductTier(), result.ProductTierType);
|
||||
Assert.Equal(organization.UsePolicies, result.UsePolicies);
|
||||
Assert.Equal(organization.UseSso, result.UseSso);
|
||||
Assert.Equal(organization.UseKeyConnector, result.UseKeyConnector);
|
||||
Assert.Equal(organization.UseScim, result.UseScim);
|
||||
Assert.Equal(organization.UseGroups, result.UseGroups);
|
||||
Assert.Equal(organization.UseDirectory, result.UseDirectory);
|
||||
Assert.Equal(organization.UseEvents, result.UseEvents);
|
||||
Assert.Equal(organization.UseTotp, result.UseTotp);
|
||||
Assert.Equal(organization.Use2fa, result.Use2fa);
|
||||
Assert.Equal(organization.UseApi, result.UseApi);
|
||||
Assert.Equal(organization.UseResetPassword, result.UseResetPassword);
|
||||
Assert.Equal(organization.UseSecretsManager, result.UseSecretsManager);
|
||||
Assert.Equal(organization.UsePasswordManager, result.UsePasswordManager);
|
||||
Assert.Equal(organization.UsersGetPremium, result.UsersGetPremium);
|
||||
Assert.Equal(organization.UseCustomPermissions, result.UseCustomPermissions);
|
||||
Assert.Equal(organization.PlanType.GetProductTier() == ProductTierType.Enterprise, result.UseActivateAutofillPolicy);
|
||||
Assert.Equal(organization.UseRiskInsights, result.UseRiskInsights);
|
||||
Assert.Equal(organization.UseOrganizationDomains, result.UseOrganizationDomains);
|
||||
Assert.Equal(organization.UseAdminSponsoredFamilies, result.UseAdminSponsoredFamilies);
|
||||
Assert.Equal(organization.UseAutomaticUserConfirmation, result.UseAutomaticUserConfirmation);
|
||||
Assert.Equal(organization.SelfHost, result.SelfHost);
|
||||
Assert.Equal(organization.Seats, result.Seats);
|
||||
Assert.Equal(organization.MaxCollections, result.MaxCollections);
|
||||
Assert.Equal(organization.MaxStorageGb, result.MaxStorageGb);
|
||||
Assert.Equal(organizationDetails.Key, result.Key);
|
||||
Assert.True(result.HasPublicAndPrivateKeys);
|
||||
Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation);
|
||||
Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion);
|
||||
Assert.Equal(organization.LimitItemDeletion, result.LimitItemDeletion);
|
||||
Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems);
|
||||
Assert.Equal(organizationDetails.ProviderId, result.ProviderId);
|
||||
Assert.Equal(organizationDetails.ProviderName, result.ProviderName);
|
||||
Assert.Equal(organizationDetails.ProviderType, result.ProviderType);
|
||||
Assert.Equal(organizationDetails.SsoEnabled, result.SsoEnabled);
|
||||
Assert.True(result.KeyConnectorEnabled);
|
||||
Assert.Equal("https://keyconnector.example.com", result.KeyConnectorUrl);
|
||||
Assert.Equal(MemberDecryptionType.KeyConnector, result.SsoMemberDecryptionType);
|
||||
Assert.True(result.SsoBound);
|
||||
Assert.Equal(organizationDetails.Status, result.Status);
|
||||
Assert.Equal(organizationDetails.Type, result.Type);
|
||||
Assert.Equal(organizationDetails.OrganizationUserId, result.OrganizationUserId);
|
||||
Assert.True(result.UserIsClaimedByOrganization);
|
||||
Assert.NotNull(result.Permissions);
|
||||
Assert.True(result.ResetPasswordEnrolled);
|
||||
Assert.Equal(organizationDetails.AccessSecretsManager, result.AccessSecretsManager);
|
||||
Assert.Equal(organizationDetails.FamilySponsorshipFriendlyName, result.FamilySponsorshipFriendlyName);
|
||||
Assert.Equal(organizationDetails.FamilySponsorshipLastSyncDate, result.FamilySponsorshipLastSyncDate);
|
||||
Assert.Equal(organizationDetails.FamilySponsorshipToDelete, result.FamilySponsorshipToDelete);
|
||||
Assert.Equal(organizationDetails.FamilySponsorshipValidUntil, result.FamilySponsorshipValidUntil);
|
||||
Assert.True(result.IsAdminInitiated);
|
||||
Assert.False(result.FamilySponsorshipAvailable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Response;
|
||||
|
||||
public class ProfileProviderOrganizationResponseModelTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void Constructor_ShouldPopulatePropertiesCorrectly(Organization organization)
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var providerId = Guid.NewGuid();
|
||||
var providerUserId = Guid.NewGuid();
|
||||
|
||||
var organizationDetails = new ProviderUserOrganizationDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = userId,
|
||||
Name = organization.Name,
|
||||
Enabled = organization.Enabled,
|
||||
Identifier = organization.Identifier,
|
||||
PlanType = organization.PlanType,
|
||||
UsePolicies = organization.UsePolicies,
|
||||
UseSso = organization.UseSso,
|
||||
UseKeyConnector = organization.UseKeyConnector,
|
||||
UseScim = organization.UseScim,
|
||||
UseGroups = organization.UseGroups,
|
||||
UseDirectory = organization.UseDirectory,
|
||||
UseEvents = organization.UseEvents,
|
||||
UseTotp = organization.UseTotp,
|
||||
Use2fa = organization.Use2fa,
|
||||
UseApi = organization.UseApi,
|
||||
UseResetPassword = organization.UseResetPassword,
|
||||
UseSecretsManager = organization.UseSecretsManager,
|
||||
UsePasswordManager = organization.UsePasswordManager,
|
||||
UsersGetPremium = organization.UsersGetPremium,
|
||||
UseCustomPermissions = organization.UseCustomPermissions,
|
||||
UseRiskInsights = organization.UseRiskInsights,
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains,
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies,
|
||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation,
|
||||
SelfHost = organization.SelfHost,
|
||||
Seats = organization.Seats,
|
||||
MaxCollections = organization.MaxCollections,
|
||||
MaxStorageGb = organization.MaxStorageGb,
|
||||
Key = "provider-org-key",
|
||||
PublicKey = "public-key",
|
||||
PrivateKey = "private-key",
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
||||
LimitItemDeletion = organization.LimitItemDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems,
|
||||
ProviderId = providerId,
|
||||
ProviderName = "Test MSP Provider",
|
||||
ProviderType = ProviderType.Msp,
|
||||
SsoEnabled = true,
|
||||
SsoConfig = new SsoConfigurationData
|
||||
{
|
||||
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption
|
||||
}.Serialize(),
|
||||
Status = ProviderUserStatusType.Confirmed,
|
||||
Type = ProviderUserType.ProviderAdmin,
|
||||
ProviderUserId = providerUserId
|
||||
};
|
||||
|
||||
var result = new ProfileProviderOrganizationResponseModel(organizationDetails);
|
||||
|
||||
Assert.Equal("profileProviderOrganization", result.Object);
|
||||
Assert.Equal(organization.Id, result.Id);
|
||||
Assert.Equal(userId, result.UserId);
|
||||
Assert.Equal(organization.Name, result.Name);
|
||||
Assert.Equal(organization.Enabled, result.Enabled);
|
||||
Assert.Equal(organization.Identifier, result.Identifier);
|
||||
Assert.Equal(organization.PlanType.GetProductTier(), result.ProductTierType);
|
||||
Assert.Equal(organization.UsePolicies, result.UsePolicies);
|
||||
Assert.Equal(organization.UseSso, result.UseSso);
|
||||
Assert.Equal(organization.UseKeyConnector, result.UseKeyConnector);
|
||||
Assert.Equal(organization.UseScim, result.UseScim);
|
||||
Assert.Equal(organization.UseGroups, result.UseGroups);
|
||||
Assert.Equal(organization.UseDirectory, result.UseDirectory);
|
||||
Assert.Equal(organization.UseEvents, result.UseEvents);
|
||||
Assert.Equal(organization.UseTotp, result.UseTotp);
|
||||
Assert.Equal(organization.Use2fa, result.Use2fa);
|
||||
Assert.Equal(organization.UseApi, result.UseApi);
|
||||
Assert.Equal(organization.UseResetPassword, result.UseResetPassword);
|
||||
Assert.Equal(organization.UseSecretsManager, result.UseSecretsManager);
|
||||
Assert.Equal(organization.UsePasswordManager, result.UsePasswordManager);
|
||||
Assert.Equal(organization.UsersGetPremium, result.UsersGetPremium);
|
||||
Assert.Equal(organization.UseCustomPermissions, result.UseCustomPermissions);
|
||||
Assert.Equal(organization.PlanType.GetProductTier() == ProductTierType.Enterprise, result.UseActivateAutofillPolicy);
|
||||
Assert.Equal(organization.UseRiskInsights, result.UseRiskInsights);
|
||||
Assert.Equal(organization.UseOrganizationDomains, result.UseOrganizationDomains);
|
||||
Assert.Equal(organization.UseAdminSponsoredFamilies, result.UseAdminSponsoredFamilies);
|
||||
Assert.Equal(organization.UseAutomaticUserConfirmation, result.UseAutomaticUserConfirmation);
|
||||
Assert.Equal(organization.SelfHost, result.SelfHost);
|
||||
Assert.Equal(organization.Seats, result.Seats);
|
||||
Assert.Equal(organization.MaxCollections, result.MaxCollections);
|
||||
Assert.Equal(organization.MaxStorageGb, result.MaxStorageGb);
|
||||
Assert.Equal(organizationDetails.Key, result.Key);
|
||||
Assert.True(result.HasPublicAndPrivateKeys);
|
||||
Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation);
|
||||
Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion);
|
||||
Assert.Equal(organization.LimitItemDeletion, result.LimitItemDeletion);
|
||||
Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems);
|
||||
Assert.Equal(organizationDetails.ProviderId, result.ProviderId);
|
||||
Assert.Equal(organizationDetails.ProviderName, result.ProviderName);
|
||||
Assert.Equal(organizationDetails.ProviderType, result.ProviderType);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, result.Status);
|
||||
Assert.Equal(OrganizationUserType.Owner, result.Type);
|
||||
Assert.Equal(organizationDetails.SsoEnabled, result.SsoEnabled);
|
||||
Assert.False(result.KeyConnectorEnabled);
|
||||
Assert.Null(result.KeyConnectorUrl);
|
||||
Assert.Equal(MemberDecryptionType.TrustedDeviceEncryption, result.SsoMemberDecryptionType);
|
||||
Assert.False(result.SsoBound);
|
||||
Assert.NotNull(result.Permissions);
|
||||
Assert.False(result.Permissions.ManageUsers);
|
||||
Assert.False(result.ResetPasswordEnrolled);
|
||||
Assert.False(result.AccessSecretsManager);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Bit.Api.AdminConsole.Public.Controllers;
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Public.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(PoliciesController))]
|
||||
[SutProviderCustomize]
|
||||
public class PoliciesControllerTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Put_WhenPolicyValidatorsRefactorEnabled_UsesVNextSavePolicyCommand(
|
||||
Guid organizationId,
|
||||
PolicyType policyType,
|
||||
PolicyUpdateRequestModel model,
|
||||
Policy policy,
|
||||
SutProvider<PoliciesController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
policy.Data = null;
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationId.Returns(organizationId);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IVNextSavePolicyCommand>()
|
||||
.SaveAsync(Arg.Any<SavePolicyModel>())
|
||||
.Returns(policy);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.Put(policyType, model);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextSavePolicyCommand>()
|
||||
.Received(1)
|
||||
.SaveAsync(Arg.Is<SavePolicyModel>(m =>
|
||||
m.PolicyUpdate.OrganizationId == organizationId &&
|
||||
m.PolicyUpdate.Type == policyType &&
|
||||
m.PolicyUpdate.Enabled == model.Enabled.GetValueOrDefault() &&
|
||||
m.PerformedBy is SystemUser));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Put_WhenPolicyValidatorsRefactorDisabled_UsesLegacySavePolicyCommand(
|
||||
Guid organizationId,
|
||||
PolicyType policyType,
|
||||
PolicyUpdateRequestModel model,
|
||||
Policy policy,
|
||||
SutProvider<PoliciesController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
policy.Data = null;
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationId.Returns(organizationId);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<ISavePolicyCommand>()
|
||||
.SaveAsync(Arg.Any<PolicyUpdate>())
|
||||
.Returns(policy);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.Put(policyType, model);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ISavePolicyCommand>()
|
||||
.Received(1)
|
||||
.SaveAsync(Arg.Is<PolicyUpdate>(p =>
|
||||
p.OrganizationId == organizationId &&
|
||||
p.Type == policyType &&
|
||||
p.Enabled == model.Enabled));
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Kdf;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -33,10 +35,10 @@ public class AccountsControllerTests : IDisposable
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||
private readonly IChangeKdfCommand _changeKdfCommand;
|
||||
|
||||
|
||||
public AccountsControllerTests()
|
||||
{
|
||||
_userService = Substitute.For<IUserService>();
|
||||
@@ -48,6 +50,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
||||
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
||||
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
|
||||
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
|
||||
|
||||
@@ -61,6 +64,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_tdeOffboardingPasswordCommand,
|
||||
_twoFactorIsEnabledQuery,
|
||||
_featureService,
|
||||
_userAccountKeysQuery,
|
||||
_twoFactorEmailService,
|
||||
_changeKdfCommand
|
||||
);
|
||||
@@ -614,6 +618,16 @@ public class AccountsControllerTests : IDisposable
|
||||
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostKdf_UserNotFound_ShouldFail(PasswordRequestModel model)
|
||||
{
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult<User>(null));
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostKdf(model));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostKdf_WithNullAuthenticationData_ShouldFail(
|
||||
@@ -623,7 +637,9 @@ public class AccountsControllerTests : IDisposable
|
||||
model.AuthenticationData = null;
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
||||
|
||||
Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -635,7 +651,41 @@ public class AccountsControllerTests : IDisposable
|
||||
model.UnlockData = null;
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
||||
|
||||
Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostKdf_ChangeKdfFailed_ShouldFail(
|
||||
User user, PasswordRequestModel model)
|
||||
{
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||
_changeKdfCommand.ChangeKdfAsync(Arg.Any<User>(), Arg.Any<string>(),
|
||||
Arg.Any<MasterPasswordAuthenticationData>(), Arg.Any<MasterPasswordUnlockData>())
|
||||
.Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = "Change KDF failed" })));
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
|
||||
|
||||
Assert.NotNull(exception.ModelState);
|
||||
Assert.Contains("Change KDF failed",
|
||||
exception.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostKdf_ChangeKdfSuccess_NoError(
|
||||
User user, PasswordRequestModel model)
|
||||
{
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||
_changeKdfCommand.ChangeKdfAsync(Arg.Any<User>(), Arg.Any<string>(),
|
||||
Arg.Any<MasterPasswordAuthenticationData>(), Arg.Any<MasterPasswordUnlockData>())
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
// Act
|
||||
await _sut.PostKdf(model);
|
||||
}
|
||||
|
||||
// Below are helper functions that currently belong to this
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Auth.Models.Request.Organizations;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Sso;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Auth.Models.Request;
|
||||
|
||||
public class OrganizationSsoRequestModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToSsoConfig_WithOrganizationId_CreatesNewSsoConfig()
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var model = new OrganizationSsoRequestModel
|
||||
{
|
||||
Enabled = true,
|
||||
Identifier = "test-identifier",
|
||||
Data = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.OpenIdConnect,
|
||||
Authority = "https://example.com",
|
||||
ClientId = "test-client",
|
||||
ClientSecret = "test-secret"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = model.ToSsoConfig(organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(organizationId, result.OrganizationId);
|
||||
Assert.True(result.Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToSsoConfig_WithExistingConfig_UpdatesExistingConfig()
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var existingConfig = new SsoConfig
|
||||
{
|
||||
Id = 1,
|
||||
OrganizationId = organizationId,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
var model = new OrganizationSsoRequestModel
|
||||
{
|
||||
Enabled = true,
|
||||
Identifier = "updated-identifier",
|
||||
Data = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.Saml2,
|
||||
IdpEntityId = "test-entity",
|
||||
IdpSingleSignOnServiceUrl = "https://sso.example.com"
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = model.ToSsoConfig(existingConfig);
|
||||
|
||||
// Assert
|
||||
Assert.Same(existingConfig, result);
|
||||
Assert.Equal(organizationId, result.OrganizationId);
|
||||
Assert.True(result.Enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public class SsoConfigurationDataRequestTests
|
||||
{
|
||||
private readonly TestI18nService _i18nService;
|
||||
private readonly ValidationContext _validationContext;
|
||||
|
||||
public SsoConfigurationDataRequestTests()
|
||||
{
|
||||
_i18nService = new TestI18nService();
|
||||
var serviceProvider = Substitute.For<IServiceProvider>();
|
||||
serviceProvider.GetService(typeof(II18nService)).Returns(_i18nService);
|
||||
_validationContext = new ValidationContext(new object(), serviceProvider, null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToConfigurationData_MapsProperties()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.OpenIdConnect,
|
||||
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
||||
Authority = "https://authority.example.com",
|
||||
ClientId = "test-client-id",
|
||||
ClientSecret = "test-client-secret",
|
||||
IdpX509PublicCert = "-----BEGIN CERTIFICATE-----\nMIIC...test\n-----END CERTIFICATE-----",
|
||||
SpOutboundSigningAlgorithm = null // Test default
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = model.ToConfigurationData();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(SsoType.OpenIdConnect, result.ConfigType);
|
||||
Assert.Equal(MemberDecryptionType.KeyConnector, result.MemberDecryptionType);
|
||||
Assert.Equal("https://authority.example.com", result.Authority);
|
||||
Assert.Equal("test-client-id", result.ClientId);
|
||||
Assert.Equal("test-client-secret", result.ClientSecret);
|
||||
Assert.Equal("MIIC...test", result.IdpX509PublicCert); // PEM headers stripped
|
||||
Assert.Equal(SamlSigningAlgorithms.Sha256, result.SpOutboundSigningAlgorithm); // Default applied
|
||||
Assert.Null(result.IdpArtifactResolutionServiceUrl); // Always null
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyConnectorEnabled_Setter_UpdatesMemberDecryptionType()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SsoConfigurationDataRequest();
|
||||
|
||||
// Act & Assert
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
model.KeyConnectorEnabled = true;
|
||||
Assert.Equal(MemberDecryptionType.KeyConnector, model.MemberDecryptionType);
|
||||
|
||||
model.KeyConnectorEnabled = false;
|
||||
Assert.Equal(MemberDecryptionType.MasterPassword, model.MemberDecryptionType);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
// Validation Tests
|
||||
[Fact]
|
||||
public void Validate_OpenIdConnect_ValidData_NoErrors()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.OpenIdConnect,
|
||||
Authority = "https://example.com",
|
||||
ClientId = "test-client",
|
||||
ClientSecret = "test-secret"
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = model.Validate(_validationContext).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", "test-client", "test-secret", "AuthorityValidationError")]
|
||||
[InlineData("https://example.com", "", "test-secret", "ClientIdValidationError")]
|
||||
[InlineData("https://example.com", "test-client", "", "ClientSecretValidationError")]
|
||||
public void Validate_OpenIdConnect_MissingRequiredFields_ReturnsErrors(string authority, string clientId, string clientSecret, string expectedError)
|
||||
{
|
||||
// Arrange
|
||||
var model = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.OpenIdConnect,
|
||||
Authority = authority,
|
||||
ClientId = clientId,
|
||||
ClientSecret = clientSecret
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = model.Validate(_validationContext).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Equal(expectedError, results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Saml2_ValidData_NoErrors()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.Saml2,
|
||||
IdpEntityId = "https://idp.example.com",
|
||||
IdpSingleSignOnServiceUrl = "https://sso.example.com",
|
||||
IdpSingleLogoutServiceUrl = "https://logout.example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = model.Validate(_validationContext).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", "https://sso.example.com", "IdpEntityIdValidationError")]
|
||||
[InlineData("not-a-valid-uri", "", "IdpSingleSignOnServiceUrlValidationError")]
|
||||
public void Validate_Saml2_MissingRequiredFields_ReturnsErrors(string entityId, string signOnUrl, string expectedError)
|
||||
{
|
||||
// Arrange
|
||||
var model = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.Saml2,
|
||||
IdpEntityId = entityId,
|
||||
IdpSingleSignOnServiceUrl = signOnUrl
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = model.Validate(_validationContext).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Contains(results, r => r.ErrorMessage == expectedError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("not-a-url")]
|
||||
[InlineData("ftp://example.com")]
|
||||
[InlineData("https://example.com<script>")]
|
||||
[InlineData("https://example.com\"onclick")]
|
||||
public void Validate_Saml2_InvalidUrls_ReturnsErrors(string invalidUrl)
|
||||
{
|
||||
// Arrange
|
||||
var model = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.Saml2,
|
||||
IdpEntityId = "https://idp.example.com",
|
||||
IdpSingleSignOnServiceUrl = invalidUrl,
|
||||
IdpSingleLogoutServiceUrl = invalidUrl
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = model.Validate(_validationContext).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Contains(results, r => r.ErrorMessage == "IdpSingleSignOnServiceUrlInvalid");
|
||||
Assert.Contains(results, r => r.ErrorMessage == "IdpSingleLogoutServiceUrlInvalid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Saml2_MissingSignOnUrl_AlwaysReturnsError()
|
||||
{
|
||||
// Arrange - SignOnUrl is always required for SAML2, regardless of EntityId format
|
||||
var model = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.Saml2,
|
||||
IdpEntityId = "https://idp.example.com", // Valid URI
|
||||
IdpSingleSignOnServiceUrl = "" // Missing - always causes error
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = model.Validate(_validationContext).ToList();
|
||||
|
||||
// Assert - Should always fail validation when SignOnUrl is missing
|
||||
Assert.Contains(results, r => r.ErrorMessage == "IdpSingleSignOnServiceUrlValidationError");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Saml2_InvalidCertificate_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.Saml2,
|
||||
IdpEntityId = "https://idp.example.com",
|
||||
IdpSingleSignOnServiceUrl = "https://sso.example.com",
|
||||
IdpX509PublicCert = "invalid-certificate-data"
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = model.Validate(_validationContext).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Contains(results, r => r.ErrorMessage.Contains("IdpX509PublicCert") && r.ErrorMessage.Contains("ValidationError"));
|
||||
}
|
||||
|
||||
// TODO: On server, make public certificate required for SAML2 SSO: https://bitwarden.atlassian.net/browse/PM-26028
|
||||
[Fact]
|
||||
public void Validate_Saml2_EmptyCertificate_PassesValidation()
|
||||
{
|
||||
// Arrange
|
||||
var model = new SsoConfigurationDataRequest
|
||||
{
|
||||
ConfigType = SsoType.Saml2,
|
||||
IdpEntityId = "https://idp.example.com",
|
||||
IdpSingleSignOnServiceUrl = "https://sso.example.com",
|
||||
IdpX509PublicCert = ""
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = model.Validate(_validationContext).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(results, r => r.MemberNames.Contains("IdpX509PublicCert"));
|
||||
}
|
||||
|
||||
private class TestI18nService : I18nService
|
||||
{
|
||||
public TestI18nService() : base(CreateMockLocalizerFactory()) { }
|
||||
|
||||
private static IStringLocalizerFactory CreateMockLocalizerFactory()
|
||||
{
|
||||
var factory = Substitute.For<IStringLocalizerFactory>();
|
||||
var localizer = Substitute.For<IStringLocalizer>();
|
||||
|
||||
localizer[Arg.Any<string>()].Returns(callInfo => new LocalizedString(callInfo.Arg<string>(), callInfo.Arg<string>()));
|
||||
localizer[Arg.Any<string>(), Arg.Any<object[]>()].Returns(callInfo => new LocalizedString(callInfo.Arg<string>(), callInfo.Arg<string>()));
|
||||
|
||||
factory.Create(Arg.Any<string>(), Arg.Any<string>()).Returns(localizer);
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
}
|
||||
800
test/Api.Test/Billing/Controllers/AccountsControllerTests.cs
Normal file
800
test/Api.Test/Billing/Controllers/AccountsControllerTests.cs
Normal file
@@ -0,0 +1,800 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.Billing.Controllers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Billing.Controllers;
|
||||
|
||||
[SubscriptionInfoCustomize]
|
||||
public class AccountsControllerTests : IDisposable
|
||||
{
|
||||
private const string TestMilestone2CouponId = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount;
|
||||
|
||||
private readonly IUserService _userService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly AccountsController _sut;
|
||||
|
||||
public AccountsControllerTests()
|
||||
{
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_paymentService = Substitute.For<IPaymentService>();
|
||||
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
||||
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
||||
_globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
|
||||
_sut = new AccountsController(
|
||||
_userService,
|
||||
_twoFactorIsEnabledQuery,
|
||||
_userAccountKeysQuery,
|
||||
_featureService
|
||||
);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_sut?.Dispose();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WhenFeatureFlagEnabled_IncludesDiscount(
|
||||
User user,
|
||||
SubscriptionInfo subscriptionInfo,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
Active = true,
|
||||
PercentOff = 20m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new List<string> { "product1" }
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);
|
||||
Assert.Equal(20m, result.CustomerDiscount.PercentOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WhenFeatureFlagDisabled_ExcludesDiscount(
|
||||
User user,
|
||||
SubscriptionInfo subscriptionInfo,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
Active = true,
|
||||
PercentOff = 20m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new List<string> { "product1" }
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(false);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.CustomerDiscount); // Should be null when feature flag is disabled
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithNonMatchingCouponId_ExcludesDiscount(
|
||||
User user,
|
||||
SubscriptionInfo subscriptionInfo,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = "different-coupon-id", // Non-matching coupon ID
|
||||
Active = true,
|
||||
PercentOff = 20m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new List<string> { "product1" }
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.CustomerDiscount); // Should be null when coupon ID doesn't match
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WhenSelfHosted_ReturnsBasicResponse(User user)
|
||||
{
|
||||
// Arrange
|
||||
var selfHostedSettings = new GlobalSettings { SelfHosted = true };
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(selfHostedSettings, _paymentService);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WhenNoGateway_ExcludesDiscount(User user, UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
user.Gateway = null; // No gateway configured
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_userService.GenerateLicenseAsync(user).Returns(license);
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.CustomerDiscount); // Should be null when no gateway
|
||||
await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithInactiveDiscount_ExcludesDiscount(
|
||||
User user,
|
||||
SubscriptionInfo subscriptionInfo,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
Active = false, // Inactive discount
|
||||
PercentOff = 20m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new List<string> { "product1" }
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.CustomerDiscount); // Should be null when discount is inactive
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_FullPipeline_ConvertsStripeDiscountToApiResponse(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Create a Stripe Discount object with real structure
|
||||
var stripeDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
PercentOff = 25m,
|
||||
AmountOff = 1400, // 1400 cents = $14.00
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = new List<string> { "prod_premium", "prod_families" }
|
||||
}
|
||||
},
|
||||
End = null // Active discount
|
||||
};
|
||||
|
||||
// Convert Stripe Discount to BillingCustomerDiscount (simulating what StripePaymentService does)
|
||||
var billingDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount);
|
||||
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = billingDiscount
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert - Verify full pipeline conversion
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
|
||||
// Verify Stripe data correctly converted to API response
|
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);
|
||||
Assert.True(result.CustomerDiscount.Active);
|
||||
Assert.Equal(25m, result.CustomerDiscount.PercentOff);
|
||||
|
||||
// Verify cents-to-dollars conversion (1400 cents -> $14.00)
|
||||
Assert.Equal(14.00m, result.CustomerDiscount.AmountOff);
|
||||
|
||||
// Verify AppliesTo products are preserved
|
||||
Assert.NotNull(result.CustomerDiscount.AppliesTo);
|
||||
Assert.Equal(2, result.CustomerDiscount.AppliesTo.Count());
|
||||
Assert.Contains("prod_premium", result.CustomerDiscount.AppliesTo);
|
||||
Assert.Contains("prod_families", result.CustomerDiscount.AppliesTo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_FullPipeline_WithFeatureFlagToggle_ControlsVisibility(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Create Stripe Discount
|
||||
var stripeDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
PercentOff = 20m
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
var billingDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount);
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = billingDiscount
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act & Assert - Feature flag ENABLED
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
var resultWithFlag = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
Assert.NotNull(resultWithFlag.CustomerDiscount);
|
||||
|
||||
// Act & Assert - Feature flag DISABLED
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(false);
|
||||
var resultWithoutFlag = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
Assert.Null(resultWithoutFlag.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_IntegrationTest_CompletePipelineFromStripeToApiResponse(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Create a real Stripe Discount object as it would come from Stripe API
|
||||
var stripeDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
PercentOff = 30m,
|
||||
AmountOff = 2000, // 2000 cents = $20.00
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = new List<string> { "prod_premium", "prod_families", "prod_teams" }
|
||||
}
|
||||
},
|
||||
End = null // Active discount (no end date)
|
||||
};
|
||||
|
||||
// Step 1: Map Stripe Discount through SubscriptionInfo.BillingCustomerDiscount
|
||||
// This simulates what StripePaymentService.GetSubscriptionAsync does
|
||||
var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount);
|
||||
|
||||
// Verify the mapping worked correctly
|
||||
Assert.Equal(TestMilestone2CouponId, billingCustomerDiscount.Id);
|
||||
Assert.True(billingCustomerDiscount.Active);
|
||||
Assert.Equal(30m, billingCustomerDiscount.PercentOff);
|
||||
Assert.Equal(20.00m, billingCustomerDiscount.AmountOff); // Converted from cents
|
||||
Assert.NotNull(billingCustomerDiscount.AppliesTo);
|
||||
Assert.Equal(3, billingCustomerDiscount.AppliesTo.Count);
|
||||
|
||||
// Step 2: Create SubscriptionInfo with the mapped discount
|
||||
// This simulates what StripePaymentService returns
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = billingCustomerDiscount
|
||||
};
|
||||
|
||||
// Step 3: Set up controller dependencies
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act - Step 4: Call AccountsController.GetSubscriptionAsync
|
||||
// This exercises the complete pipeline:
|
||||
// - Retrieves subscriptionInfo from paymentService (with discount from Stripe)
|
||||
// - Maps through SubscriptionInfo.BillingCustomerDiscount (already done above)
|
||||
// - Filters in SubscriptionResponseModel constructor (based on feature flag, coupon ID, active status)
|
||||
// - Returns via AccountsController
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert - Verify the complete pipeline worked end-to-end
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
|
||||
// Verify Stripe Discount → SubscriptionInfo.BillingCustomerDiscount mapping
|
||||
// (verified above, but confirming it made it through)
|
||||
|
||||
// Verify SubscriptionInfo.BillingCustomerDiscount → SubscriptionResponseModel.BillingCustomerDiscount filtering
|
||||
// The filter should pass because:
|
||||
// - includeMilestone2Discount = true (feature flag enabled)
|
||||
// - subscription.CustomerDiscount != null
|
||||
// - subscription.CustomerDiscount.Id == Milestone2SubscriptionDiscount
|
||||
// - subscription.CustomerDiscount.Active = true
|
||||
Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id);
|
||||
Assert.True(result.CustomerDiscount.Active);
|
||||
Assert.Equal(30m, result.CustomerDiscount.PercentOff);
|
||||
Assert.Equal(20.00m, result.CustomerDiscount.AmountOff); // Verify cents-to-dollars conversion
|
||||
|
||||
// Verify AppliesTo products are preserved through the entire pipeline
|
||||
Assert.NotNull(result.CustomerDiscount.AppliesTo);
|
||||
Assert.Equal(3, result.CustomerDiscount.AppliesTo.Count());
|
||||
Assert.Contains("prod_premium", result.CustomerDiscount.AppliesTo);
|
||||
Assert.Contains("prod_families", result.CustomerDiscount.AppliesTo);
|
||||
Assert.Contains("prod_teams", result.CustomerDiscount.AppliesTo);
|
||||
|
||||
// Verify the payment service was called correctly
|
||||
await _paymentService.Received(1).GetSubscriptionAsync(user);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_IntegrationTest_MultipleDiscountsInSubscription_PrefersCustomerDiscount(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Create Stripe subscription with multiple discounts
|
||||
// Customer discount should be preferred over subscription discounts
|
||||
var customerDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
PercentOff = 30m,
|
||||
AmountOff = null
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
var subscriptionDiscount1 = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = "other-coupon-1",
|
||||
PercentOff = 10m
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
var subscriptionDiscount2 = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = "other-coupon-2",
|
||||
PercentOff = 15m
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Map through SubscriptionInfo.BillingCustomerDiscount
|
||||
var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(customerDiscount);
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = billingCustomerDiscount
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert - Should use customer discount, not subscription discounts
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id);
|
||||
Assert.Equal(30m, result.CustomerDiscount.PercentOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_IntegrationTest_BothPercentOffAndAmountOffPresent_HandlesEdgeCase(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Edge case: Stripe coupon with both PercentOff and AmountOff
|
||||
// This tests the scenario mentioned in BillingCustomerDiscountTests.cs line 212-232
|
||||
var stripeDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
PercentOff = 25m,
|
||||
AmountOff = 2000, // 2000 cents = $20.00
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = new List<string> { "prod_premium" }
|
||||
}
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
// Map through SubscriptionInfo.BillingCustomerDiscount
|
||||
var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount);
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = billingCustomerDiscount
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert - Both values should be preserved through the pipeline
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id);
|
||||
Assert.Equal(25m, result.CustomerDiscount.PercentOff);
|
||||
Assert.Equal(20.00m, result.CustomerDiscount.AmountOff); // Converted from cents
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_IntegrationTest_BillingSubscriptionMapsThroughPipeline(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Create Stripe subscription with subscription details
|
||||
var stripeSubscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
TrialStart = DateTime.UtcNow.AddDays(-30),
|
||||
TrialEnd = DateTime.UtcNow.AddDays(-20),
|
||||
CanceledAt = null,
|
||||
CancelAtPeriodEnd = false,
|
||||
CollectionMethod = "charge_automatically"
|
||||
};
|
||||
|
||||
// Map through SubscriptionInfo.BillingSubscription
|
||||
var billingSubscription = new SubscriptionInfo.BillingSubscription(stripeSubscription);
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
Subscription = billingSubscription,
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
Active = true,
|
||||
PercentOff = 20m
|
||||
}
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert - Verify BillingSubscription mapped through pipeline
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Subscription);
|
||||
Assert.Equal("active", result.Subscription.Status);
|
||||
Assert.Equal(14, result.Subscription.GracePeriod); // charge_automatically = 14 days
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_IntegrationTest_BillingUpcomingInvoiceMapsThroughPipeline(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Create Stripe invoice for upcoming invoice
|
||||
var stripeInvoice = new Invoice
|
||||
{
|
||||
AmountDue = 2000, // 2000 cents = $20.00
|
||||
Created = DateTime.UtcNow.AddDays(1)
|
||||
};
|
||||
|
||||
// Map through SubscriptionInfo.BillingUpcomingInvoice
|
||||
var billingUpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(stripeInvoice);
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
UpcomingInvoice = billingUpcomingInvoice,
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
Active = true,
|
||||
PercentOff = 20m
|
||||
}
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert - Verify BillingUpcomingInvoice mapped through pipeline
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.UpcomingInvoice);
|
||||
Assert.Equal(20.00m, result.UpcomingInvoice.Amount); // Converted from cents
|
||||
Assert.NotNull(result.UpcomingInvoice.Date);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_IntegrationTest_CompletePipelineWithAllComponents(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Complete Stripe objects for full pipeline test
|
||||
var stripeDiscount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = TestMilestone2CouponId,
|
||||
PercentOff = 20m,
|
||||
AmountOff = 1000, // $10.00
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = new List<string> { "prod_premium", "prod_families" }
|
||||
}
|
||||
},
|
||||
End = null
|
||||
};
|
||||
|
||||
var stripeSubscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
CollectionMethod = "charge_automatically"
|
||||
};
|
||||
|
||||
var stripeInvoice = new Invoice
|
||||
{
|
||||
AmountDue = 1500, // $15.00
|
||||
Created = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
// Map through SubscriptionInfo (simulating StripePaymentService)
|
||||
var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount);
|
||||
var billingSubscription = new SubscriptionInfo.BillingSubscription(stripeSubscription);
|
||||
var billingUpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(stripeInvoice);
|
||||
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = billingCustomerDiscount,
|
||||
Subscription = billingSubscription,
|
||||
UpcomingInvoice = billingUpcomingInvoice
|
||||
};
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act - Full pipeline: Stripe → SubscriptionInfo → SubscriptionResponseModel → API response
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert - Verify all components mapped correctly through the pipeline
|
||||
Assert.NotNull(result);
|
||||
|
||||
// Verify discount
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id);
|
||||
Assert.Equal(20m, result.CustomerDiscount.PercentOff);
|
||||
Assert.Equal(10.00m, result.CustomerDiscount.AmountOff);
|
||||
Assert.NotNull(result.CustomerDiscount.AppliesTo);
|
||||
Assert.Equal(2, result.CustomerDiscount.AppliesTo.Count());
|
||||
|
||||
// Verify subscription
|
||||
Assert.NotNull(result.Subscription);
|
||||
Assert.Equal("active", result.Subscription.Status);
|
||||
Assert.Equal(14, result.Subscription.GracePeriod);
|
||||
|
||||
// Verify upcoming invoice
|
||||
Assert.NotNull(result.UpcomingInvoice);
|
||||
Assert.Equal(15.00m, result.UpcomingInvoice.Amount);
|
||||
Assert.NotNull(result.UpcomingInvoice.Date);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_SelfHosted_WithDiscountFlagEnabled_NeverIncludesDiscount(User user)
|
||||
{
|
||||
// Arrange - Self-hosted user with discount flag enabled (should still return null)
|
||||
var selfHostedSettings = new GlobalSettings { SelfHosted = true };
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); // Flag enabled
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(selfHostedSettings, _paymentService);
|
||||
|
||||
// Assert - Should never include discount for self-hosted, even with flag enabled
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_NullGateway_WithDiscountFlagEnabled_NeverIncludesDiscount(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - User with null gateway and discount flag enabled (should still return null)
|
||||
user.Gateway = null; // No gateway configured
|
||||
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = claimsPrincipal }
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_userService.GenerateLicenseAsync(user).Returns(license);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); // Flag enabled
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
|
||||
// Assert - Should never include discount when no gateway, even with flag enabled
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Api.Billing.Controllers;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
@@ -53,19 +52,16 @@ public class OrganizationBillingControllerTests
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
|
||||
.Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null, 0));
|
||||
.Returns(new OrganizationMetadata(true, 10));
|
||||
|
||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||
|
||||
Assert.IsType<Ok<OrganizationMetadataResponse>>(result);
|
||||
Assert.IsType<Ok<OrganizationMetadata>>(result);
|
||||
|
||||
var response = ((Ok<OrganizationMetadataResponse>)result).Value;
|
||||
var response = ((Ok<OrganizationMetadata>)result).Value;
|
||||
|
||||
Assert.True(response.IsEligibleForSelfHost);
|
||||
Assert.True(response.IsManaged);
|
||||
Assert.True(response.IsOnSecretsManagerStandalone);
|
||||
Assert.True(response.IsSubscriptionUnpaid);
|
||||
Assert.True(response.HasSubscription);
|
||||
Assert.Equal(10, response.OrganizationOccupiedSeats);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
@@ -270,7 +271,6 @@ public class ProviderBillingControllerTests
|
||||
var subscription = new Subscription
|
||||
{
|
||||
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
|
||||
CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
@@ -291,20 +291,23 @@ public class ProviderBillingControllerTests
|
||||
Data = [
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),
|
||||
Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }
|
||||
},
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),
|
||||
Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams }
|
||||
}
|
||||
]
|
||||
},
|
||||
Status = "unpaid",
|
||||
Status = "unpaid"
|
||||
};
|
||||
|
||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
|
||||
options =>
|
||||
options.Expand.Contains("customer.tax_ids") &&
|
||||
options.Expand.Contains("discounts") &&
|
||||
options.Expand.Contains("test_clock"))).Returns(subscription);
|
||||
|
||||
var daysInLastMonth = DateTime.DaysInMonth(oneMonthAgo.Year, oneMonthAgo.Month);
|
||||
@@ -365,7 +368,7 @@ public class ProviderBillingControllerTests
|
||||
var response = ((Ok<ProviderSubscriptionResponse>)result).Value;
|
||||
|
||||
Assert.Equal(subscription.Status, response.Status);
|
||||
Assert.Equal(subscription.CurrentPeriodEnd, response.CurrentPeriodEndDate);
|
||||
Assert.Equal(subscription.GetCurrentPeriodEnd(), response.CurrentPeriodEndDate);
|
||||
Assert.Equal(subscription.Customer!.Discount!.Coupon!.PercentOff, response.DiscountPercentage);
|
||||
Assert.Equal(subscription.CollectionMethod, response.CollectionMethod);
|
||||
|
||||
@@ -405,6 +408,118 @@ public class ProviderBillingControllerTests
|
||||
Assert.Equal(14, response.Suspension.GracePeriod);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetSubscriptionAsync_SubscriptionLevelDiscount_Ok(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingController> sutProvider)
|
||||
{
|
||||
ConfigureStableProviderServiceUserInputs(provider, sutProvider);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var oneMonthAgo = now.AddMonths(-1);
|
||||
|
||||
var daysInThisMonth = DateTime.DaysInMonth(now.Year, now.Month);
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
|
||||
Customer = new Customer
|
||||
{
|
||||
Address = new Address
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345",
|
||||
Line1 = "123 Example St.",
|
||||
Line2 = "Unit 1",
|
||||
City = "Example Town",
|
||||
State = "NY"
|
||||
},
|
||||
Balance = -100000,
|
||||
Discount = null, // No customer-level discount
|
||||
TaxIds = new StripeList<TaxId> { Data = [new TaxId { Value = "123456789" }] }
|
||||
},
|
||||
Discounts =
|
||||
[
|
||||
new Discount { Coupon = new Coupon { PercentOff = 15 } } // Subscription-level discount
|
||||
],
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = [
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),
|
||||
Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }
|
||||
},
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),
|
||||
Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams }
|
||||
}
|
||||
]
|
||||
},
|
||||
Status = "active"
|
||||
};
|
||||
|
||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
|
||||
options =>
|
||||
options.Expand.Contains("customer.tax_ids") &&
|
||||
options.Expand.Contains("discounts") &&
|
||||
options.Expand.Contains("test_clock"))).Returns(subscription);
|
||||
|
||||
stripeAdapter.InvoiceSearchAsync(Arg.Is<InvoiceSearchOptions>(
|
||||
options => options.Query == $"subscription:'{subscription.Id}' status:'open'"))
|
||||
.Returns([]);
|
||||
|
||||
var providerPlans = new List<ProviderPlan>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ProviderId = provider.Id,
|
||||
PlanType = PlanType.TeamsMonthly,
|
||||
SeatMinimum = 50,
|
||||
PurchasedSeats = 10,
|
||||
AllocatedSeats = 60
|
||||
},
|
||||
new ()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ProviderId = provider.Id,
|
||||
PlanType = PlanType.EnterpriseMonthly,
|
||||
SeatMinimum = 100,
|
||||
PurchasedSeats = 0,
|
||||
AllocatedSeats = 90
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
foreach (var providerPlan in providerPlans)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);
|
||||
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);
|
||||
sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId)
|
||||
.Returns(new Price
|
||||
{
|
||||
UnitAmountDecimal = plan.PasswordManager.ProviderPortalSeatPrice * 100
|
||||
});
|
||||
}
|
||||
|
||||
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
|
||||
|
||||
Assert.IsType<Ok<ProviderSubscriptionResponse>>(result);
|
||||
|
||||
var response = ((Ok<ProviderSubscriptionResponse>)result).Value;
|
||||
|
||||
Assert.Equal(subscription.Status, response.Status);
|
||||
Assert.Equal(subscription.GetCurrentPeriodEnd(), response.CurrentPeriodEndDate);
|
||||
Assert.Equal(15, response.DiscountPercentage); // Verify subscription-level discount is used
|
||||
Assert.Equal(subscription.CollectionMethod, response.CollectionMethod);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateTaxInformationAsync
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
@@ -455,4 +460,98 @@ public class PoliciesControllerTests
|
||||
Assert.Equal(enabledPolicy.Type, expectedPolicy.Type);
|
||||
Assert.Equal(enabledPolicy.Enabled, expectedPolicy.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutVNext_WhenPolicyValidatorsRefactorEnabled_UsesVNextSavePolicyCommand(
|
||||
SutProvider<PoliciesController> sutProvider, Guid orgId,
|
||||
SavePolicyRequest model, Policy policy, Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
policy.Data = null;
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId
|
||||
.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(orgId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IVNextSavePolicyCommand>()
|
||||
.SaveAsync(Arg.Any<SavePolicyModel>())
|
||||
.Returns(policy);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.PutVNext(orgId, policy.Type, model);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVNextSavePolicyCommand>()
|
||||
.Received(1)
|
||||
.SaveAsync(Arg.Is<SavePolicyModel>(
|
||||
m => m.PolicyUpdate.OrganizationId == orgId &&
|
||||
m.PolicyUpdate.Type == policy.Type &&
|
||||
m.PolicyUpdate.Enabled == model.Policy.Enabled &&
|
||||
m.PerformedBy.UserId == userId &&
|
||||
m.PerformedBy.IsOrganizationOwnerOrProvider == true));
|
||||
|
||||
await sutProvider.GetDependency<ISavePolicyCommand>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.VNextSaveAsync(default);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(policy.Id, result.Id);
|
||||
Assert.Equal(policy.Type, result.Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PutVNext_WhenPolicyValidatorsRefactorDisabled_UsesSavePolicyCommand(
|
||||
SutProvider<PoliciesController> sutProvider, Guid orgId,
|
||||
SavePolicyRequest model, Policy policy, Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
policy.Data = null;
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.UserId
|
||||
.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(orgId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<ISavePolicyCommand>()
|
||||
.VNextSaveAsync(Arg.Any<SavePolicyModel>())
|
||||
.Returns(policy);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.PutVNext(orgId, policy.Type, model);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ISavePolicyCommand>()
|
||||
.Received(1)
|
||||
.VNextSaveAsync(Arg.Is<SavePolicyModel>(
|
||||
m => m.PolicyUpdate.OrganizationId == orgId &&
|
||||
m.PolicyUpdate.Type == policy.Type &&
|
||||
m.PolicyUpdate.Enabled == model.Policy.Enabled &&
|
||||
m.PerformedBy.UserId == userId &&
|
||||
m.PerformedBy.IsOrganizationOwnerOrProvider == true));
|
||||
|
||||
await sutProvider.GetDependency<IVNextSavePolicyCommand>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.SaveAsync(default);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(policy.Id, result.Id);
|
||||
Assert.Equal(policy.Type, result.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Api.Dirt.Controllers;
|
||||
using Bit.Api.Dirt.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Models.Data;
|
||||
@@ -39,7 +40,8 @@ public class OrganizationReportControllerTests
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
||||
Assert.Equal(expectedReport, okResult.Value);
|
||||
var expectedResponse = new OrganizationReportResponseModel(expectedReport);
|
||||
Assert.Equivalent(expectedResponse, okResult.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -262,7 +264,8 @@ public class OrganizationReportControllerTests
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
||||
Assert.Equal(expectedReport, okResult.Value);
|
||||
var expectedResponse = new OrganizationReportResponseModel(expectedReport);
|
||||
Assert.Equivalent(expectedResponse, okResult.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -365,7 +368,8 @@ public class OrganizationReportControllerTests
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
||||
Assert.Equal(expectedReport, okResult.Value);
|
||||
var expectedResponse = new OrganizationReportResponseModel(expectedReport);
|
||||
Assert.Equivalent(expectedResponse, okResult.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -597,7 +601,8 @@ public class OrganizationReportControllerTests
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
||||
Assert.Equal(expectedReport, okResult.Value);
|
||||
var expectedResponse = new OrganizationReportResponseModel(expectedReport);
|
||||
Assert.Equivalent(expectedResponse, okResult.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -812,7 +817,8 @@ public class OrganizationReportControllerTests
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
||||
Assert.Equal(expectedReport, okResult.Value);
|
||||
var expectedResponse = new OrganizationReportResponseModel(expectedReport);
|
||||
Assert.Equivalent(expectedResponse, okResult.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -1050,7 +1056,8 @@ public class OrganizationReportControllerTests
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsType<OkObjectResult>(result);
|
||||
Assert.Equal(expectedReport, okResult.Value);
|
||||
var expectedResponse = new OrganizationReportResponseModel(expectedReport);
|
||||
Assert.Equivalent(expectedResponse, okResult.Value);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
@@ -110,6 +110,7 @@ public class AccountsKeyManagementControllerTests
|
||||
public async Task RotateUserAccountKeysSuccess(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
@@ -142,8 +143,60 @@ public class AccountsKeyManagementControllerTests
|
||||
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
||||
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
||||
|
||||
&& d.AccountPublicKey == data.AccountKeys.AccountPublicKey
|
||||
&& d.UserKeyEncryptedAccountPrivateKey == data.AccountKeys.UserKeyEncryptedAccountPrivateKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeys_UserCryptoV2_Success_Async(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel
|
||||
{
|
||||
SignatureAlgorithm = "ed25519",
|
||||
WrappedSigningKey = "wrappedSigningKey",
|
||||
VerifyingKey = "verifyingKey"
|
||||
};
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.EmergencyAccessUnlockData));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.OrganizationAccountRecoveryUnlockData));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.PasskeyUnlockData));
|
||||
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Ciphers));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Folders));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Sends));
|
||||
|
||||
await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)
|
||||
.RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>
|
||||
d.OldMasterKeyAuthenticationHash == data.OldMasterKeyAuthenticationHash
|
||||
|
||||
&& d.MasterPasswordUnlockData.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType
|
||||
&& d.MasterPasswordUnlockData.KdfIterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations
|
||||
&& d.MasterPasswordUnlockData.KdfMemory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory
|
||||
&& d.MasterPasswordUnlockData.KdfParallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism
|
||||
&& d.MasterPasswordUnlockData.Email == data.AccountUnlockData.MasterPasswordUnlockData.Email
|
||||
|
||||
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
||||
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
||||
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.SignedPublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey
|
||||
&& d.AccountKeys!.SignatureKeyPairData!.SignatureAlgorithm == Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519
|
||||
&& d.AccountKeys!.SignatureKeyPairData.WrappedSigningKey == data.AccountKeys.SignatureKeyPair!.WrappedSigningKey
|
||||
&& d.AccountKeys!.SignatureKeyPairData.VerifyingKey == data.AccountKeys.SignatureKeyPair!.VerifyingKey
|
||||
));
|
||||
}
|
||||
|
||||
@@ -153,6 +206,7 @@ public class AccountsKeyManagementControllerTests
|
||||
public async Task RotateUserKeyNoUser_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = null;
|
||||
User? user = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
@@ -165,6 +219,7 @@ public class AccountsKeyManagementControllerTests
|
||||
public async Task RotateUserKeyWrongData_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user, IdentityErrorDescriber _identityErrorDescriber)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()));
|
||||
|
||||
112
test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs
Normal file
112
test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
#nullable enable
|
||||
using Bit.Api.KeyManagement.Controllers;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.KeyManagement.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(UsersController))]
|
||||
[SutProviderCustomize]
|
||||
[JsonDocumentCustomize]
|
||||
public class UsersControllerTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetPublicKey_NotFound_ThrowsNotFoundException(
|
||||
SutProvider<UsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(Arg.Any<Guid>()).ReturnsNull();
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetPublicKeyAsync(new Guid()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetPublicKey_ReturnsUserKeyResponseModel(
|
||||
SutProvider<UsersController> sutProvider,
|
||||
Guid userId)
|
||||
{
|
||||
var publicKey = "publicKey";
|
||||
sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(userId).Returns(publicKey);
|
||||
|
||||
var result = await sutProvider.Sut.GetPublicKeyAsync(userId);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(userId, result.UserId);
|
||||
Assert.Equal(publicKey, result.PublicKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountKeys_UserNotFound_ThrowsNotFoundException(
|
||||
SutProvider<UsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).ReturnsNull();
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAccountKeysAsync(new Guid()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel(
|
||||
SutProvider<UsersController> sutProvider,
|
||||
Guid userId)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = userId,
|
||||
PublicKey = "publicKey",
|
||||
SignedPublicKey = "signedPublicKey",
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(userId).Returns(user);
|
||||
sutProvider.GetDependency<IUserAccountKeysQuery>()
|
||||
.Run(user)
|
||||
.Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("wrappedPrivateKey", "publicKey", "signedPublicKey"),
|
||||
SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "wrappedSigningKey", "verifyingKey"),
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.GetAccountKeysAsync(userId);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("publicKey", result.PublicKey);
|
||||
Assert.Equal("signedPublicKey", result.SignedPublicKey);
|
||||
Assert.Equal("verifyingKey", result.VerifyingKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel_WithNullVerifyingKey(
|
||||
SutProvider<UsersController> sutProvider,
|
||||
Guid userId)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = userId,
|
||||
PublicKey = "publicKey",
|
||||
SignedPublicKey = null,
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(userId).Returns(user);
|
||||
sutProvider.GetDependency<IUserAccountKeysQuery>()
|
||||
.Run(user)
|
||||
.Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("wrappedPrivateKey", "publicKey", null),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.GetAccountKeysAsync(userId);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("publicKey", result.PublicKey);
|
||||
Assert.Null(result.SignedPublicKey);
|
||||
Assert.Null(result.VerifyingKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.KeyManagement.Models.Request;
|
||||
|
||||
public class SignatureKeyPairRequestModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToSignatureKeyPairData_WrongAlgorithm_Rejects()
|
||||
{
|
||||
var model = new SignatureKeyPairRequestModel
|
||||
{
|
||||
SignatureAlgorithm = "abc",
|
||||
WrappedSigningKey = "wrappedKey",
|
||||
VerifyingKey = "verifyingKey"
|
||||
};
|
||||
|
||||
Assert.Throws<ArgumentException>(() => model.ToSignatureKeyPairData());
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,9 @@ public class WebAuthnLoginKeyRotationValidatorTests
|
||||
{
|
||||
Id = guid,
|
||||
SupportsPrf = true,
|
||||
EncryptedPublicKey = "TestKey",
|
||||
EncryptedUserKey = "Test"
|
||||
EncryptedPublicKey = "TestPublicKey",
|
||||
EncryptedUserKey = "TestUserKey",
|
||||
EncryptedPrivateKey = "TestPrivateKey"
|
||||
};
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<WebAuthnCredential> { data });
|
||||
@@ -45,8 +46,12 @@ public class WebAuthnLoginKeyRotationValidatorTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_DoesNotSupportPRF_Ignores(
|
||||
[BitAutoData(false, null, null, null)]
|
||||
[BitAutoData(true, null, "TestPublicKey", "TestPrivateKey")]
|
||||
[BitAutoData(true, "TestUserKey", null, "TestPrivateKey")]
|
||||
[BitAutoData(true, "TestUserKey", "TestPublicKey", null)]
|
||||
public async Task ValidateAsync_NotEncryptedOrPrfNotSupported_Ignores(
|
||||
bool supportsPrf, string encryptedUserKey, string encryptedPublicKey, string encryptedPrivateKey,
|
||||
SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,
|
||||
IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)
|
||||
{
|
||||
@@ -58,7 +63,14 @@ public class WebAuthnLoginKeyRotationValidatorTests
|
||||
EncryptedPublicKey = e.EncryptedPublicKey,
|
||||
}).ToList();
|
||||
|
||||
var data = new WebAuthnCredential { Id = guid, EncryptedUserKey = "Test", EncryptedPublicKey = "TestKey" };
|
||||
var data = new WebAuthnCredential
|
||||
{
|
||||
Id = guid,
|
||||
SupportsPrf = supportsPrf,
|
||||
EncryptedUserKey = encryptedUserKey,
|
||||
EncryptedPublicKey = encryptedPublicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<WebAuthnCredential> { data });
|
||||
@@ -69,7 +81,7 @@ public class WebAuthnLoginKeyRotationValidatorTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WrongWebAuthnKeys_Throws(
|
||||
public async Task ValidateAsync_WebAuthnKeysNotMatchingExisting_Throws(
|
||||
SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,
|
||||
IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)
|
||||
{
|
||||
@@ -84,10 +96,12 @@ public class WebAuthnLoginKeyRotationValidatorTests
|
||||
{
|
||||
Id = Guid.Parse("00000000-0000-0000-0000-000000000002"),
|
||||
SupportsPrf = true,
|
||||
EncryptedPublicKey = "TestKey",
|
||||
EncryptedUserKey = "Test"
|
||||
EncryptedPublicKey = "TestPublicKey",
|
||||
EncryptedUserKey = "TestUserKey",
|
||||
EncryptedPrivateKey = "TestPrivateKey"
|
||||
};
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data });
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<WebAuthnCredential> { data });
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));
|
||||
@@ -100,20 +114,24 @@ public class WebAuthnLoginKeyRotationValidatorTests
|
||||
IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel
|
||||
{
|
||||
Id = guid,
|
||||
EncryptedPublicKey = e.EncryptedPublicKey,
|
||||
}).ToList();
|
||||
var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e =>
|
||||
new WebAuthnLoginRotateKeyRequestModel
|
||||
{
|
||||
Id = guid,
|
||||
EncryptedPublicKey = e.EncryptedPublicKey,
|
||||
EncryptedUserKey = null
|
||||
}).ToList();
|
||||
|
||||
var data = new WebAuthnCredential
|
||||
{
|
||||
Id = guid,
|
||||
SupportsPrf = true,
|
||||
EncryptedPublicKey = "TestKey",
|
||||
EncryptedUserKey = "Test"
|
||||
EncryptedPublicKey = "TestPublicKey",
|
||||
EncryptedUserKey = "TestUserKey",
|
||||
EncryptedPrivateKey = "TestPrivateKey"
|
||||
};
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data });
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<WebAuthnCredential> { data });
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));
|
||||
@@ -131,19 +149,21 @@ public class WebAuthnLoginKeyRotationValidatorTests
|
||||
{
|
||||
Id = guid,
|
||||
EncryptedUserKey = e.EncryptedUserKey,
|
||||
EncryptedPublicKey = null,
|
||||
}).ToList();
|
||||
|
||||
var data = new WebAuthnCredential
|
||||
{
|
||||
Id = guid,
|
||||
SupportsPrf = true,
|
||||
EncryptedPublicKey = "TestKey",
|
||||
EncryptedUserKey = "Test"
|
||||
EncryptedPublicKey = "TestPublicKey",
|
||||
EncryptedUserKey = "TestUserKey",
|
||||
EncryptedPrivateKey = "TestPrivateKey"
|
||||
};
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data });
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<WebAuthnCredential> { data });
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
400
test/Api.Test/Models/Response/SubscriptionResponseModelTests.cs
Normal file
400
test/Api.Test/Models/Response/SubscriptionResponseModelTests.cs
Normal file
@@ -0,0 +1,400 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Models.Response;
|
||||
|
||||
public class SubscriptionResponseModelTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_IncludeMilestone2DiscountTrueMatchingCouponId_ReturnsDiscount(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID
|
||||
Active = true,
|
||||
PercentOff = 20m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new List<string> { "product1" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);
|
||||
Assert.True(result.CustomerDiscount.Active);
|
||||
Assert.Equal(20m, result.CustomerDiscount.PercentOff);
|
||||
Assert.Null(result.CustomerDiscount.AmountOff);
|
||||
Assert.NotNull(result.CustomerDiscount.AppliesTo);
|
||||
Assert.Single(result.CustomerDiscount.AppliesTo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_IncludeMilestone2DiscountTrueNonMatchingCouponId_ReturnsNull(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = "different-coupon-id", // Non-matching coupon ID
|
||||
Active = true,
|
||||
PercentOff = 20m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new List<string> { "product1" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_IncludeMilestone2DiscountFalseMatchingCouponId_ReturnsNull(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID
|
||||
Active = true,
|
||||
PercentOff = 20m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new List<string> { "product1" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: false);
|
||||
|
||||
// Assert - Should be null because includeMilestone2Discount is false
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_NullCustomerDiscount_ReturnsNull(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_AmountOffDiscountMatchingCouponId_ReturnsDiscount(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,
|
||||
Active = true,
|
||||
PercentOff = null,
|
||||
AmountOff = 14.00m, // Already converted from cents in BillingCustomerDiscount
|
||||
AppliesTo = new List<string>()
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);
|
||||
Assert.Null(result.CustomerDiscount.PercentOff);
|
||||
Assert.Equal(14.00m, result.CustomerDiscount.AmountOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_DefaultIncludeMilestone2DiscountParameter_ReturnsNull(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,
|
||||
Active = true,
|
||||
PercentOff = 20m
|
||||
}
|
||||
};
|
||||
|
||||
// Act - Using default parameter (includeMilestone2Discount defaults to false)
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_NullDiscountIdIncludeMilestone2DiscountTrue_ReturnsNull(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = null, // Null discount ID
|
||||
Active = true,
|
||||
PercentOff = 20m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new List<string> { "product1" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_MatchingCouponIdInactiveDiscount_ReturnsNull(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID
|
||||
Active = false, // Inactive discount
|
||||
PercentOff = 20m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new List<string> { "product1" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_UserOnly_SetsBasicProperties(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Storage = 5368709120; // 5 GB in bytes
|
||||
user.MaxStorageGb = (short)10;
|
||||
user.PremiumExpirationDate = DateTime.UtcNow.AddMonths(12);
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.StorageName);
|
||||
Assert.Equal(5.0, result.StorageGb);
|
||||
Assert.Equal((short)10, result.MaxStorageGb);
|
||||
Assert.Equal(user.PremiumExpirationDate, result.Expiration);
|
||||
Assert.Null(result.License);
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_UserAndLicense_IncludesLicense(User user, UserLicense license)
|
||||
{
|
||||
// Arrange
|
||||
user.Storage = 1073741824; // 1 GB in bytes
|
||||
user.MaxStorageGb = (short)5;
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, license);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result.License);
|
||||
Assert.Equal(license, result.License);
|
||||
Assert.Equal(1.0, result.StorageGb);
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_NullStorage_SetsStorageToZero(User user)
|
||||
{
|
||||
// Arrange
|
||||
user.Storage = null;
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.StorageName);
|
||||
Assert.Equal(0, result.StorageGb);
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_NullLicense_ExcludesLicense(User user)
|
||||
{
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, null);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result.License);
|
||||
Assert.Null(result.CustomerDiscount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_BothPercentOffAndAmountOffPresent_HandlesEdgeCase(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Edge case: Both PercentOff and AmountOff present
|
||||
// This tests the scenario where Stripe coupon has both discount types
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,
|
||||
Active = true,
|
||||
PercentOff = 25m,
|
||||
AmountOff = 20.00m, // Already converted from cents
|
||||
AppliesTo = new List<string> { "prod_premium" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);
|
||||
|
||||
// Assert - Both values should be preserved
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);
|
||||
Assert.Equal(25m, result.CustomerDiscount.PercentOff);
|
||||
Assert.Equal(20.00m, result.CustomerDiscount.AmountOff);
|
||||
Assert.NotNull(result.CustomerDiscount.AppliesTo);
|
||||
Assert.Single(result.CustomerDiscount.AppliesTo);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_WithSubscriptionAndInvoice_MapsAllProperties(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Test with Subscription, UpcomingInvoice, and CustomerDiscount
|
||||
var stripeSubscription = new Subscription
|
||||
{
|
||||
Id = "sub_test123",
|
||||
Status = "active",
|
||||
CollectionMethod = "charge_automatically"
|
||||
};
|
||||
|
||||
var stripeInvoice = new Invoice
|
||||
{
|
||||
AmountDue = 1500, // 1500 cents = $15.00
|
||||
Created = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
Subscription = new SubscriptionInfo.BillingSubscription(stripeSubscription),
|
||||
UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(stripeInvoice),
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,
|
||||
Active = true,
|
||||
PercentOff = 20m,
|
||||
AmountOff = null,
|
||||
AppliesTo = new List<string> { "prod_premium" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);
|
||||
|
||||
// Assert - Verify all properties are mapped correctly
|
||||
Assert.NotNull(result.Subscription);
|
||||
Assert.Equal("active", result.Subscription.Status);
|
||||
Assert.Equal(14, result.Subscription.GracePeriod); // charge_automatically = 14 days
|
||||
|
||||
Assert.NotNull(result.UpcomingInvoice);
|
||||
Assert.Equal(15.00m, result.UpcomingInvoice.Amount);
|
||||
Assert.NotNull(result.UpcomingInvoice.Date);
|
||||
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);
|
||||
Assert.True(result.CustomerDiscount.Active);
|
||||
Assert.Equal(20m, result.CustomerDiscount.PercentOff);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void Constructor_WithNullSubscriptionAndInvoice_HandlesNullsGracefully(
|
||||
User user,
|
||||
UserLicense license)
|
||||
{
|
||||
// Arrange - Test with null Subscription and UpcomingInvoice
|
||||
var subscriptionInfo = new SubscriptionInfo
|
||||
{
|
||||
Subscription = null,
|
||||
UpcomingInvoice = null,
|
||||
CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,
|
||||
Active = true,
|
||||
PercentOff = 20m
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);
|
||||
|
||||
// Assert - Null Subscription and UpcomingInvoice should be handled gracefully
|
||||
Assert.Null(result.Subscription);
|
||||
Assert.Null(result.UpcomingInvoice);
|
||||
Assert.NotNull(result.CustomerDiscount);
|
||||
}
|
||||
}
|
||||
@@ -361,7 +361,7 @@ public class ServiceAccountsControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task BulkDelete_ReturnsAccessDeniedForProjectsWithoutAccess_Success(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data)
|
||||
public async Task BulkDelete_ReturnsAccessDeniedForProjectsWithoutAccess_Success(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data, Guid userId)
|
||||
{
|
||||
var ids = data.Select(sa => sa.Id).ToList();
|
||||
var organizationId = data.First().OrganizationId;
|
||||
@@ -377,6 +377,7 @@ public class ServiceAccountsControllerTests
|
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
|
||||
var results = await sutProvider.Sut.BulkDeleteAsync(ids);
|
||||
|
||||
@@ -390,7 +391,7 @@ public class ServiceAccountsControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task BulkDelete_Success(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data)
|
||||
public async Task BulkDelete_Success(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data, Guid userId)
|
||||
{
|
||||
var ids = data.Select(sa => sa.Id).ToList();
|
||||
var organizationId = data.First().OrganizationId;
|
||||
@@ -404,6 +405,7 @@ public class ServiceAccountsControllerTests
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IServiceAccountRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
|
||||
var results = await sutProvider.Sut.BulkDeleteAsync(ids);
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ public class ImportCiphersControllerTests
|
||||
.With(x => x.Ciphers, fixture.Build<CipherRequestModel>()
|
||||
.With(c => c.OrganizationId, Guid.NewGuid().ToString())
|
||||
.With(c => c.FolderId, Guid.NewGuid().ToString())
|
||||
.With(c => c.ArchivedDate, (DateTime?)null)
|
||||
.CreateMany(1).ToArray())
|
||||
.Create();
|
||||
|
||||
@@ -92,6 +93,37 @@ public class ImportCiphersControllerTests
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostImportIndividual_WithArchivedDate_SavesArchivedDate(User user,
|
||||
IFixture fixture, SutProvider<ImportCiphersController> sutProvider)
|
||||
{
|
||||
var archivedDate = DateTime.UtcNow;
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.SelfHosted = false;
|
||||
|
||||
sutProvider.GetDependency<Core.Services.IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user.Id);
|
||||
|
||||
var request = fixture.Build<ImportCiphersRequestModel>()
|
||||
.With(x => x.Ciphers, fixture.Build<CipherRequestModel>()
|
||||
.With(c => c.ArchivedDate, archivedDate)
|
||||
.With(c => c.FolderId, (string)null)
|
||||
.CreateMany(1).ToArray())
|
||||
.Create();
|
||||
|
||||
await sutProvider.Sut.PostImport(request);
|
||||
|
||||
await sutProvider.GetDependency<IImportCiphersCommand>()
|
||||
.Received()
|
||||
.ImportIntoIndividualVaultAsync(
|
||||
Arg.Any<List<Folder>>(),
|
||||
Arg.Is<List<CipherDetails>>(ciphers => ciphers.First().ArchivedDate == archivedDate),
|
||||
Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),
|
||||
user.Id
|
||||
);
|
||||
}
|
||||
|
||||
/****************************
|
||||
* PostImport - Organization
|
||||
****************************/
|
||||
@@ -156,6 +188,7 @@ public class ImportCiphersControllerTests
|
||||
.With(x => x.Ciphers, fixture.Build<CipherRequestModel>()
|
||||
.With(c => c.OrganizationId, Guid.NewGuid().ToString())
|
||||
.With(c => c.FolderId, Guid.NewGuid().ToString())
|
||||
.With(c => c.ArchivedDate, (DateTime?)null)
|
||||
.CreateMany(1).ToArray())
|
||||
.With(y => y.Collections, fixture.Build<CollectionWithIdRequestModel>()
|
||||
.With(c => c.Id, orgIdGuid)
|
||||
@@ -227,6 +260,7 @@ public class ImportCiphersControllerTests
|
||||
.With(x => x.Ciphers, fixture.Build<CipherRequestModel>()
|
||||
.With(c => c.OrganizationId, Guid.NewGuid().ToString())
|
||||
.With(c => c.FolderId, Guid.NewGuid().ToString())
|
||||
.With(c => c.ArchivedDate, (DateTime?)null)
|
||||
.CreateMany(1).ToArray())
|
||||
.With(y => y.Collections, fixture.Build<CollectionWithIdRequestModel>()
|
||||
.With(c => c.Id, orgIdGuid)
|
||||
@@ -291,6 +325,7 @@ public class ImportCiphersControllerTests
|
||||
.With(x => x.Ciphers, fixture.Build<CipherRequestModel>()
|
||||
.With(c => c.OrganizationId, Guid.NewGuid().ToString())
|
||||
.With(c => c.FolderId, Guid.NewGuid().ToString())
|
||||
.With(c => c.ArchivedDate, (DateTime?)null)
|
||||
.CreateMany(1).ToArray())
|
||||
.With(y => y.Collections, fixture.Build<CollectionWithIdRequestModel>()
|
||||
.With(c => c.Id, orgIdGuid)
|
||||
@@ -354,6 +389,7 @@ public class ImportCiphersControllerTests
|
||||
.With(x => x.Ciphers, fixture.Build<CipherRequestModel>()
|
||||
.With(c => c.OrganizationId, Guid.NewGuid().ToString())
|
||||
.With(c => c.FolderId, Guid.NewGuid().ToString())
|
||||
.With(c => c.ArchivedDate, (DateTime?)null)
|
||||
.CreateMany(1).ToArray())
|
||||
.With(y => y.Collections, fixture.Build<CollectionWithIdRequestModel>()
|
||||
.With(c => c.Id, orgIdGuid)
|
||||
@@ -423,6 +459,7 @@ public class ImportCiphersControllerTests
|
||||
Ciphers = fixture.Build<CipherRequestModel>()
|
||||
.With(_ => _.OrganizationId, orgId.ToString())
|
||||
.With(_ => _.FolderId, Guid.NewGuid().ToString())
|
||||
.With(_ => _.ArchivedDate, (DateTime?)null)
|
||||
.CreateMany(2).ToArray(),
|
||||
CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),
|
||||
};
|
||||
@@ -499,6 +536,7 @@ public class ImportCiphersControllerTests
|
||||
Ciphers = fixture.Build<CipherRequestModel>()
|
||||
.With(_ => _.OrganizationId, orgId.ToString())
|
||||
.With(_ => _.FolderId, Guid.NewGuid().ToString())
|
||||
.With(_ => _.ArchivedDate, (DateTime?)null)
|
||||
.CreateMany(2).ToArray(),
|
||||
CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),
|
||||
};
|
||||
@@ -578,6 +616,7 @@ public class ImportCiphersControllerTests
|
||||
Ciphers = fixture.Build<CipherRequestModel>()
|
||||
.With(_ => _.OrganizationId, orgId.ToString())
|
||||
.With(_ => _.FolderId, Guid.NewGuid().ToString())
|
||||
.With(_ => _.ArchivedDate, (DateTime?)null)
|
||||
.CreateMany(2).ToArray(),
|
||||
CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),
|
||||
};
|
||||
@@ -651,6 +690,7 @@ public class ImportCiphersControllerTests
|
||||
Ciphers = fixture.Build<CipherRequestModel>()
|
||||
.With(_ => _.OrganizationId, orgId.ToString())
|
||||
.With(_ => _.FolderId, Guid.NewGuid().ToString())
|
||||
.With(_ => _.ArchivedDate, (DateTime?)null)
|
||||
.CreateMany(2).ToArray(),
|
||||
CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),
|
||||
};
|
||||
@@ -720,6 +760,7 @@ public class ImportCiphersControllerTests
|
||||
Ciphers = fixture.Build<CipherRequestModel>()
|
||||
.With(_ => _.OrganizationId, orgId.ToString())
|
||||
.With(_ => _.FolderId, Guid.NewGuid().ToString())
|
||||
.With(_ => _.ArchivedDate, (DateTime?)null)
|
||||
.CreateMany(2).ToArray(),
|
||||
CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),
|
||||
};
|
||||
@@ -765,6 +806,63 @@ public class ImportCiphersControllerTests
|
||||
Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostImportOrganization_ThrowsException_WhenAnyCipherIsArchived(
|
||||
SutProvider<ImportCiphersController> sutProvider,
|
||||
IFixture fixture,
|
||||
User user
|
||||
)
|
||||
{
|
||||
var orgId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.SelfHosted = false;
|
||||
sutProvider.GetDependency<GlobalSettings>()
|
||||
.ImportCiphersLimitation = _organizationCiphersLimitations;
|
||||
|
||||
SetupUserService(sutProvider, user);
|
||||
|
||||
var ciphers = fixture.Build<CipherRequestModel>()
|
||||
.With(_ => _.ArchivedDate, DateTime.UtcNow)
|
||||
.CreateMany(2).ToArray();
|
||||
|
||||
var request = new ImportOrganizationCiphersRequestModel
|
||||
{
|
||||
Collections = new List<CollectionWithIdRequestModel>().ToArray(),
|
||||
Ciphers = ciphers,
|
||||
CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.AccessImportExport(Arg.Any<Guid>())
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
||||
Arg.Any<IEnumerable<Collection>>(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>
|
||||
reqs.Contains(BulkCollectionOperations.ImportCiphers)))
|
||||
.Returns(AuthorizationResult.Failed());
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
||||
Arg.Any<IEnumerable<Collection>>(),
|
||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>
|
||||
reqs.Contains(BulkCollectionOperations.Create)))
|
||||
.Returns(AuthorizationResult.Success());
|
||||
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.GetManyByOrganizationIdAsync(orgId)
|
||||
.Returns(new List<Collection>());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
{
|
||||
await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);
|
||||
});
|
||||
|
||||
Assert.Equal("You cannot import archived items into an organization.", exception.Message);
|
||||
}
|
||||
|
||||
private static void SetupUserService(SutProvider<ImportCiphersController> sutProvider, User user)
|
||||
{
|
||||
// This is a workaround for the NSubstitute issue with ambiguous arguments
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
using Bit.Api.Models.Public.Request;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Api.Utilities.DiagnosticTools;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Utilities.DiagnosticTools;
|
||||
|
||||
public class EventDiagnosticLoggerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void LogAggregateData_WithPublicResponse_FeatureFlagEnabled_LogsInformation(
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
var logger = Substitute.For<ILogger>();
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true);
|
||||
|
||||
var request = new EventFilterRequestModel()
|
||||
{
|
||||
Start = DateTime.UtcNow.AddMinutes(-3),
|
||||
End = DateTime.UtcNow,
|
||||
ActingUserId = Guid.NewGuid(),
|
||||
ItemId = Guid.NewGuid(),
|
||||
};
|
||||
|
||||
var newestEvent = Substitute.For<IEvent>();
|
||||
newestEvent.Date.Returns(DateTime.UtcNow);
|
||||
var middleEvent = Substitute.For<IEvent>();
|
||||
middleEvent.Date.Returns(DateTime.UtcNow.AddDays(-1));
|
||||
var oldestEvent = Substitute.For<IEvent>();
|
||||
oldestEvent.Date.Returns(DateTime.UtcNow.AddDays(-3));
|
||||
|
||||
var eventResponses = new List<EventResponseModel>
|
||||
{
|
||||
new (newestEvent),
|
||||
new (middleEvent),
|
||||
new (oldestEvent)
|
||||
};
|
||||
var response = new PagedListResponseModel<EventResponseModel>(eventResponses, "continuation-token");
|
||||
|
||||
// Act
|
||||
logger.LogAggregateData(featureService, organizationId, response, request);
|
||||
|
||||
// Assert
|
||||
logger.Received(1).Log(
|
||||
LogLevel.Information,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o =>
|
||||
o.ToString().Contains(organizationId.ToString()) &&
|
||||
o.ToString().Contains($"Event count:{eventResponses.Count}") &&
|
||||
o.ToString().Contains($"newest record:{newestEvent.Date:O}") &&
|
||||
o.ToString().Contains($"oldest record:{oldestEvent.Date:O}") &&
|
||||
o.ToString().Contains("HasMore:True") &&
|
||||
o.ToString().Contains($"Start:{request.Start:o}") &&
|
||||
o.ToString().Contains($"End:{request.End:o}") &&
|
||||
o.ToString().Contains($"ActingUserId:{request.ActingUserId}") &&
|
||||
o.ToString().Contains($"ItemId:{request.ItemId}"))
|
||||
,
|
||||
null,
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void LogAggregateData_WithPublicResponse_FeatureFlagDisabled_DoesNotLog(
|
||||
Guid organizationId,
|
||||
EventFilterRequestModel request)
|
||||
{
|
||||
// Arrange
|
||||
var logger = Substitute.For<ILogger>();
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(false);
|
||||
|
||||
PagedListResponseModel<EventResponseModel> dummy = null;
|
||||
|
||||
// Act
|
||||
logger.LogAggregateData(featureService, organizationId, dummy, request);
|
||||
|
||||
// Assert
|
||||
logger.DidNotReceive().Log(
|
||||
LogLevel.Information,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void LogAggregateData_WithPublicResponse_EmptyData_LogsZeroCount(
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
var logger = Substitute.For<ILogger>();
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true);
|
||||
|
||||
var request = new EventFilterRequestModel()
|
||||
{
|
||||
Start = null,
|
||||
End = null,
|
||||
ActingUserId = null,
|
||||
ItemId = null,
|
||||
ContinuationToken = null,
|
||||
};
|
||||
var response = new PagedListResponseModel<EventResponseModel>(new List<EventResponseModel>(), null);
|
||||
|
||||
// Act
|
||||
logger.LogAggregateData(featureService, organizationId, response, request);
|
||||
|
||||
// Assert
|
||||
logger.Received(1).Log(
|
||||
LogLevel.Information,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o =>
|
||||
o.ToString().Contains(organizationId.ToString()) &&
|
||||
o.ToString().Contains("Event count:0") &&
|
||||
o.ToString().Contains("HasMore:False")),
|
||||
null,
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void LogAggregateData_WithInternalResponse_FeatureFlagDisabled_DoesNotLog(Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
var logger = Substitute.For<ILogger>();
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(false);
|
||||
|
||||
|
||||
// Act
|
||||
logger.LogAggregateData(featureService, organizationId, null, null, null, null);
|
||||
|
||||
// Assert
|
||||
logger.DidNotReceive().Log(
|
||||
LogLevel.Information,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void LogAggregateData_WithInternalResponse_EmptyData_LogsZeroCount(
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
var logger = Substitute.For<ILogger>();
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true);
|
||||
|
||||
Bit.Api.Models.Response.EventResponseModel[] emptyEvents = [];
|
||||
|
||||
// Act
|
||||
logger.LogAggregateData(featureService, organizationId, emptyEvents, null, null, null);
|
||||
|
||||
// Assert
|
||||
logger.Received(1).Log(
|
||||
LogLevel.Information,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o =>
|
||||
o.ToString().Contains(organizationId.ToString()) &&
|
||||
o.ToString().Contains("Event count:0") &&
|
||||
o.ToString().Contains("HasMore:False")),
|
||||
null,
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void LogAggregateData_WithInternalResponse_FeatureFlagEnabled_LogsInformation(
|
||||
Guid organizationId)
|
||||
{
|
||||
// Arrange
|
||||
var logger = Substitute.For<ILogger>();
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true);
|
||||
|
||||
var newestEvent = Substitute.For<IEvent>();
|
||||
newestEvent.Date.Returns(DateTime.UtcNow);
|
||||
var middleEvent = Substitute.For<IEvent>();
|
||||
middleEvent.Date.Returns(DateTime.UtcNow.AddDays(-1));
|
||||
var oldestEvent = Substitute.For<IEvent>();
|
||||
oldestEvent.Date.Returns(DateTime.UtcNow.AddDays(-2));
|
||||
|
||||
var events = new List<Bit.Api.Models.Response.EventResponseModel>
|
||||
{
|
||||
new (newestEvent),
|
||||
new (middleEvent),
|
||||
new (oldestEvent)
|
||||
};
|
||||
|
||||
var queryStart = DateTime.UtcNow.AddMinutes(-3);
|
||||
var queryEnd = DateTime.UtcNow;
|
||||
const string continuationToken = "continuation-token";
|
||||
|
||||
// Act
|
||||
logger.LogAggregateData(featureService, organizationId, events, continuationToken, queryStart, queryEnd);
|
||||
|
||||
// Assert
|
||||
logger.Received(1).Log(
|
||||
LogLevel.Information,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o =>
|
||||
o.ToString().Contains(organizationId.ToString()) &&
|
||||
o.ToString().Contains($"Event count:{events.Count}") &&
|
||||
o.ToString().Contains($"newest record:{newestEvent.Date:O}") &&
|
||||
o.ToString().Contains($"oldest record:{oldestEvent.Date:O}") &&
|
||||
o.ToString().Contains("HasMore:True") &&
|
||||
o.ToString().Contains($"Start:{queryStart:o}") &&
|
||||
o.ToString().Contains($"End:{queryEnd:o}"))
|
||||
,
|
||||
null,
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
}
|
||||
}
|
||||
@@ -1790,6 +1790,118 @@ public class CiphersControllerTests
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShareMany_ArchivedCipher_ThrowsBadRequestException(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
CipherWithIdRequestModel request,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
request.EncryptedFor = userId;
|
||||
request.OrganizationId = organizationId.ToString();
|
||||
request.ArchivedDate = DateTime.UtcNow;
|
||||
var model = new CipherBulkShareRequestModel
|
||||
{
|
||||
Ciphers = [request],
|
||||
CollectionIds = [Guid.NewGuid().ToString()]
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(Task.FromResult(true));
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(default)
|
||||
.ReturnsForAnyArgs(userId);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.PutShareMany(model)
|
||||
);
|
||||
|
||||
Assert.Equal("Cannot move archived items to an organization.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShareMany_ExistingCipherArchived_ThrowsBadRequestException(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
CipherWithIdRequestModel request,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
// Request model does not have ArchivedDate (only the existing cipher does)
|
||||
request.EncryptedFor = userId;
|
||||
request.OrganizationId = organizationId.ToString();
|
||||
request.ArchivedDate = null;
|
||||
|
||||
var model = new CipherBulkShareRequestModel
|
||||
{
|
||||
Ciphers = [request],
|
||||
CollectionIds = [Guid.NewGuid().ToString()]
|
||||
};
|
||||
|
||||
// The existing cipher from the repository IS archived
|
||||
var existingCipher = new CipherDetails
|
||||
{
|
||||
Id = request.Id!.Value,
|
||||
UserId = userId,
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new CipherLoginData()),
|
||||
ArchivedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(Task.FromResult(true));
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(default)
|
||||
.ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetManyByUserIdAsync(userId, withOrganizations: false)
|
||||
.Returns(Task.FromResult((ICollection<CipherDetails>)[existingCipher]));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.PutShareMany(model)
|
||||
);
|
||||
|
||||
Assert.Equal("Cannot move archived items to an organization.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PutShare_ArchivedCipher_ThrowsBadRequestException(
|
||||
Guid cipherId,
|
||||
Guid organizationId,
|
||||
User user,
|
||||
CipherShareRequestModel model,
|
||||
SutProvider<CiphersController> sutProvider)
|
||||
{
|
||||
model.Cipher.OrganizationId = organizationId.ToString();
|
||||
model.Cipher.EncryptedFor = user.Id;
|
||||
|
||||
var cipher = new Cipher
|
||||
{
|
||||
Id = cipherId,
|
||||
UserId = user.Id,
|
||||
ArchivedDate = DateTime.UtcNow.AddDays(-1),
|
||||
Type = CipherType.Login,
|
||||
Data = JsonSerializer.Serialize(new CipherLoginData())
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(user);
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.GetByIdAsync(cipherId)
|
||||
.Returns(cipher);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationUser(organizationId)
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.PutShare(cipherId, model)
|
||||
);
|
||||
|
||||
Assert.Equal("Cannot move an archived item to an organization.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostPurge_WhenUserNotFound_ThrowsUnauthorizedAccessException(
|
||||
SecretVerificationRequestModel model,
|
||||
|
||||
@@ -12,6 +12,8 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -74,6 +76,7 @@ public class SyncControllerTests
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
|
||||
// Adjust random data to match required formats / test intentions
|
||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||
@@ -98,6 +101,11 @@ public class SyncControllerTests
|
||||
|
||||
// Setup returns
|
||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
organizationUserRepository
|
||||
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
|
||||
@@ -127,7 +135,6 @@ public class SyncControllerTests
|
||||
// Execute GET
|
||||
var result = await sutProvider.Sut.Get();
|
||||
|
||||
|
||||
// Asserts
|
||||
// Assert that methods are called
|
||||
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
|
||||
@@ -166,6 +173,7 @@ public class SyncControllerTests
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
|
||||
// Adjust random data to match required formats / test intentions
|
||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||
@@ -189,6 +197,11 @@ public class SyncControllerTests
|
||||
|
||||
// Setup returns
|
||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
organizationUserRepository
|
||||
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
|
||||
@@ -256,6 +269,7 @@ public class SyncControllerTests
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
|
||||
// Adjust random data to match required formats / test intentions
|
||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||
@@ -271,6 +285,10 @@ public class SyncControllerTests
|
||||
providerUserRepository
|
||||
.GetManyDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed).Returns(providerUserDetails);
|
||||
|
||||
foreach (var p in providerUserOrganizationDetails)
|
||||
{
|
||||
p.SsoConfig = null;
|
||||
}
|
||||
providerUserRepository
|
||||
.GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed)
|
||||
.Returns(providerUserOrganizationDetails);
|
||||
@@ -290,6 +308,12 @@ public class SyncControllerTests
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
|
||||
userService.HasPremiumFromOrganization(user).Returns(false);
|
||||
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
// Execute GET
|
||||
var result = await sutProvider.Sut.Get();
|
||||
|
||||
@@ -327,6 +351,13 @@ public class SyncControllerTests
|
||||
|
||||
user.MasterPassword = null;
|
||||
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||
|
||||
@@ -352,6 +383,13 @@ public class SyncControllerTests
|
||||
user.KdfMemory = kdfMemory;
|
||||
user.KdfParallelism = kdfParallelism;
|
||||
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user