1
0
mirror of https://github.com/bitwarden/server synced 2026-01-02 00:23:40 +00:00

Merge branch 'main' into auth/pm-22975/client-version-validator

This commit is contained in:
Patrick Pimentel
2025-12-15 11:38:43 -05:00
144 changed files with 4400 additions and 2857 deletions

View File

@@ -1,18 +1,14 @@
using System.Text.Json;
using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Mvc;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Controllers;
@@ -25,823 +21,191 @@ public class OrganizationIntegrationsConfigurationControllerTests
public async Task DeleteAsync_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration)
Guid integrationId,
Guid configurationId)
{
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.DeleteAsync(organizationId, organizationIntegration.Id, organizationIntegrationConfiguration.Id);
await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);
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);
await sutProvider.GetDependency<IDeleteOrganizationIntegrationConfigurationCommand>().Received(1)
.DeleteAsync(organizationId, integrationId, configurationId);
}
[Theory, BitAutoData]
[Obsolete("Obsolete")]
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,
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.DeleteAsync(organizationId, Guid.Empty, Guid.Empty));
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
Guid integrationId,
Guid configurationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty));
}
await sutProvider.Sut.PostDeleteAsync(organizationId, integrationId, configurationId);
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, organizationIntegration.Id, Guid.Empty));
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationConfigDoesNotBelongToIntegration_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = Guid.Empty;
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 Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, organizationIntegration.Id, Guid.Empty));
await sutProvider.GetDependency<IDeleteOrganizationIntegrationConfigurationCommand>().Received(1)
.DeleteAsync(organizationId, integrationId, configurationId);
}
[Theory, BitAutoData]
public async Task DeleteAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
Guid organizationId,
Guid integrationId,
Guid configurationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty));
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
}
[Theory, BitAutoData]
public async Task GetAsync_ConfigurationsExist_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
List<OrganizationIntegrationConfiguration> organizationIntegrationConfigurations)
Guid integrationId,
List<OrganizationIntegrationConfiguration> configurations)
{
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>()
.GetManyByIntegrationAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfigurations);
sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>()
.GetManyByIntegrationAsync(organizationId, integrationId)
.Returns(configurations);
var result = await sutProvider.Sut.GetAsync(organizationId, integrationId);
var result = await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id);
Assert.NotNull(result);
Assert.Equal(organizationIntegrationConfigurations.Count, result.Count);
Assert.Equal(configurations.Count, result.Count);
Assert.All(result, r => Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(r));
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>().Received(1)
.GetManyByIntegrationAsync(organizationId, integrationId);
}
[Theory, BitAutoData]
public async Task GetAsync_NoConfigurationsExist_ReturnsEmptyList(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
Guid integrationId)
{
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>()
.GetManyByIntegrationAsync(Arg.Any<Guid>())
sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>()
.GetManyByIntegrationAsync(organizationId, integrationId)
.Returns([]);
var result = await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id);
var result = await sutProvider.Sut.GetAsync(organizationId, integrationId);
Assert.NotNull(result);
Assert.Empty(result);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(organizationIntegration.Id);
}
[Theory, BitAutoData]
public async Task GetAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.NewGuid()));
}
[Theory, BitAutoData]
public async Task GetAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id));
await sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>().Received(1)
.GetManyByIntegrationAsync(organizationId, integrationId);
}
[Theory, BitAutoData]
public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
Guid organizationId,
Guid integrationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.NewGuid()));
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.GetAsync(organizationId, integrationId));
}
[Theory, BitAutoData]
public async Task PostAsync_AllParamsProvided_Slack_Succeeds(
public async Task PostAsync_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
Guid integrationId,
OrganizationIntegrationConfiguration configuration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Slack;
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
model.Configuration = JsonSerializer.Serialize(slackConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
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>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
sutProvider.GetDependency<ICreateOrganizationIntegrationConfigurationCommand>()
.CreateAsync(organizationId, integrationId, Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(configuration);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, integrationId, model);
await sutProvider.GetDependency<ICreateOrganizationIntegrationConfigurationCommand>().Received(1)
.CreateAsync(organizationId, integrationId, Arg.Any<OrganizationIntegrationConfiguration>());
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]
public async Task PostAsync_AllParamsProvided_Webhook_Succeeds(
public async Task PostAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
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>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
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]
public async Task PostAsync_OnlyUrlProvided_Webhook_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"));
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
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>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
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]
public async Task PostAsync_IntegrationTypeCloudBillingSync_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.CloudBillingSync;
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>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
organizationIntegration.Id,
model));
}
[Theory, BitAutoData]
public async Task PostAsync_IntegrationTypeScim_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Scim;
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>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
organizationIntegration.Id,
model));
}
[Theory, BitAutoData]
public async Task PostAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
Guid.Empty,
new OrganizationIntegrationConfigurationRequestModel()));
}
[Theory, BitAutoData]
public async Task PostAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
organizationIntegration.Id,
new OrganizationIntegrationConfigurationRequestModel()));
}
[Theory, BitAutoData]
public async Task PostAsync_InvalidConfiguration_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
model.Configuration = null;
model.Template = "Template String";
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>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
organizationIntegration.Id,
model));
}
[Theory, BitAutoData]
public async Task PostAsync_InvalidTemplate_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = null;
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>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
organizationId,
organizationIntegration.Id,
model));
}
[Theory, BitAutoData]
public async Task PostAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<OrganizationIntegrationConfigurationController> sutProvider, Guid organizationId)
Guid integrationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(organizationId, Guid.Empty, new OrganizationIntegrationConfigurationRequestModel()));
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.CreateAsync(organizationId, integrationId, new OrganizationIntegrationConfigurationRequestModel()));
}
[Theory, BitAutoData]
public async Task UpdateAsync_AllParamsProvided_Slack_Succeeds(
public async Task UpdateAsync_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
Guid integrationId,
Guid configurationId,
OrganizationIntegrationConfiguration configuration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Slack;
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
model.Configuration = JsonSerializer.Serialize(slackConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
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);
var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model);
sutProvider.GetDependency<IUpdateOrganizationIntegrationConfigurationCommand>()
.UpdateAsync(organizationId, integrationId, configurationId, Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(configuration);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
var updateResponse = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, model);
await sutProvider.GetDependency<IUpdateOrganizationIntegrationConfigurationCommand>().Received(1)
.UpdateAsync(organizationId, integrationId, configurationId, Arg.Any<OrganizationIntegrationConfiguration>());
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]
public async Task UpdateAsync_AllParamsProvided_Webhook_Succeeds(
public async Task UpdateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
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);
var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
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]
public async Task UpdateAsync_OnlyUrlProvided_Webhook_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"));
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
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);
var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
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]
public async Task UpdateAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
model.Filters = null;
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.UpdateAsync(
organizationId,
organizationIntegration.Id,
Guid.Empty,
model));
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(
organizationId,
Guid.Empty,
Guid.Empty,
new OrganizationIntegrationConfigurationRequestModel()));
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
Guid.Empty,
new OrganizationIntegrationConfigurationRequestModel()));
}
[Theory, BitAutoData]
public async Task UpdateAsync_InvalidConfiguration_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Slack;
model.Configuration = null;
model.Template = "Template String";
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 Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model));
}
[Theory, BitAutoData]
public async Task UpdateAsync_InvalidTemplate_ThrowsBadRequestException(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
OrganizationIntegrationConfigurationRequestModel model)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Slack;
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
model.Configuration = JsonSerializer.Serialize(slackConfig);
model.Template = null;
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 Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
model));
}
[Theory, BitAutoData]
public async Task UpdateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<OrganizationIntegrationConfigurationController> sutProvider, Guid organizationId)
Guid integrationId,
Guid configurationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(
organizationId,
Guid.Empty,
Guid.Empty,
new OrganizationIntegrationConfigurationRequestModel()));
await Assert.ThrowsAsync<NotFoundException>(async () =>
await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, new OrganizationIntegrationConfigurationRequestModel()));
}
}

View File

@@ -1,248 +0,0 @@
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;
public class OrganizationIntegrationConfigurationRequestModelTests
{
[Fact]
public void IsValidForType_CloudBillingSyncIntegration_ReturnsFalse()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = "{}",
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.CloudBillingSync));
}
[Theory]
[InlineData(data: null)]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyConfiguration_ReturnsFalse(string? config)
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Theory]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyNonNullConfiguration_ReturnsFalse(string? config)
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
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_NullConfiguration_ReturnsTrue()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = null,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Hec));
Assert.True(condition: model.IsValidForType(IntegrationType.Datadog));
Assert.True(condition: model.IsValidForType(IntegrationType.Teams));
}
[Theory]
[InlineData(data: null)]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyTemplate_ReturnsFalse(string? template)
{
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = template
};
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]
public void IsValidForType_InvalidJsonConfiguration_ReturnsFalse()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = "{not valid json}",
Template = "template"
};
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]
public void IsValidForType_InvalidJsonFilters_ReturnsFalse()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://example.com")));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Filters = "{Not valid json",
Template = "template"
};
Assert.False(model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_ScimIntegration_ReturnsFalse()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = "{}",
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.Scim));
}
[Fact]
public void IsValidForType_ValidSlackConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(value: new SlackIntegrationConfiguration(ChannelId: "C12345"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Slack));
}
[Fact]
public void IsValidForType_ValidSlackConfigurationWithFilters_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345"));
var filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
Rules = [
new IntegrationFilterRule()
{
Operation = IntegrationFilterOperation.Equals,
Property = "CollectionId",
Value = Guid.NewGuid()
}
],
Groups = []
});
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Filters = filters,
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Slack));
}
[Fact]
public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost")));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_ValidWebhookConfigurationWithFilters_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(
Uri: new Uri("https://example.com"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
Rules = [
new IntegrationFilterRule()
{
Operation = IntegrationFilterOperation.Equals,
Property = "CollectionId",
Value = Guid.NewGuid()
}
],
Groups = []
});
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Filters = filters,
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_UnknownIntegrationType_ReturnsFalse()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = "{}",
Template = "template"
};
var unknownType = (IntegrationType)999;
Assert.False(condition: model.IsValidForType(unknownType));
}
}

View File

@@ -28,7 +28,7 @@ public class AccountsControllerTests : IDisposable
private readonly IUserService _userService;
private readonly IFeatureService _featureService;
private readonly IPaymentService _paymentService;
private readonly IStripePaymentService _paymentService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly ILicensingService _licensingService;
@@ -39,7 +39,7 @@ public class AccountsControllerTests : IDisposable
{
_userService = Substitute.For<IUserService>();
_featureService = Substitute.For<IFeatureService>();
_paymentService = Substitute.For<IPaymentService>();
_paymentService = Substitute.For<IStripePaymentService>();
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_licensingService = Substitute.For<ILicensingService>();

View File

@@ -3,9 +3,9 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Http.HttpResults;
@@ -103,7 +103,7 @@ public class OrganizationBillingControllerTests
// Manually create a BillingHistoryInfo object to avoid requiring AutoFixture to create HttpResponseHeaders
var billingInfo = new BillingHistoryInfo();
sutProvider.GetDependency<IPaymentService>().GetBillingHistoryAsync(organization).Returns(billingInfo);
sutProvider.GetDependency<IStripePaymentService>().GetBillingHistoryAsync(organization).Returns(billingInfo);
// Act
var result = await sutProvider.Sut.GetHistoryAsync(organizationId);

View File

@@ -37,7 +37,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPaymentService _paymentService;
private readonly IStripePaymentService _paymentService;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IUserService _userService;
private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery;
@@ -59,7 +59,7 @@ public class OrganizationsControllerTests : IDisposable
_organizationRepository = Substitute.For<IOrganizationRepository>();
_organizationService = Substitute.For<IOrganizationService>();
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_paymentService = Substitute.For<IPaymentService>();
_paymentService = Substitute.For<IStripePaymentService>();
Substitute.For<IPolicyRepository>();
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
Substitute.For<ISsoConfigService>();

View File

@@ -10,10 +10,10 @@ using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Models.Api;
using Bit.Core.Models.BitStripe;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -121,7 +121,7 @@ public class ProviderBillingControllerTests
}
};
sutProvider.GetDependency<IStripeAdapter>().InvoiceListAsync(Arg.Is<StripeInvoiceListOptions>(
sutProvider.GetDependency<IStripeAdapter>().ListInvoicesAsync(Arg.Is<StripeInvoiceListOptions>(
options =>
options.Customer == provider.GatewayCustomerId)).Returns(invoices);
@@ -301,7 +301,7 @@ public class ProviderBillingControllerTests
Status = "unpaid"
};
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
stripeAdapter.GetSubscriptionAsync(provider.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
options =>
options.Expand.Contains("customer.tax_ids") &&
options.Expand.Contains("discounts") &&
@@ -318,7 +318,7 @@ public class ProviderBillingControllerTests
Attempted = true
};
stripeAdapter.InvoiceSearchAsync(Arg.Is<InvoiceSearchOptions>(
stripeAdapter.SearchInvoiceAsync(Arg.Is<InvoiceSearchOptions>(
options => options.Query == $"subscription:'{subscription.Id}' status:'open'"))
.Returns([overdueInvoice]);
@@ -351,7 +351,7 @@ public class ProviderBillingControllerTests
var plan = MockPlans.Get(providerPlan.PlanType);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);
sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId)
sutProvider.GetDependency<IStripeAdapter>().GetPriceAsync(priceId)
.Returns(new Price
{
UnitAmountDecimal = plan.PasswordManager.ProviderPortalSeatPrice * 100
@@ -459,13 +459,13 @@ public class ProviderBillingControllerTests
Status = "active"
};
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
stripeAdapter.GetSubscriptionAsync(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>(
stripeAdapter.SearchInvoiceAsync(Arg.Is<InvoiceSearchOptions>(
options => options.Query == $"subscription:'{subscription.Id}' status:'open'"))
.Returns([]);
@@ -498,7 +498,7 @@ public class ProviderBillingControllerTests
var plan = MockPlans.Get(providerPlan.PlanType);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);
sutProvider.GetDependency<IStripeAdapter>().PriceGetAsync(priceId)
sutProvider.GetDependency<IStripeAdapter>().GetPriceAsync(priceId)
.Returns(new Price
{
UnitAmountDecimal = plan.PasswordManager.ProviderPortalSeatPrice * 100

View File

@@ -31,7 +31,7 @@ public class BitPayControllerTests
private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>();
private readonly IProviderRepository _providerRepository = Substitute.For<IProviderRepository>();
private readonly IMailService _mailService = Substitute.For<IMailService>();
private readonly IPaymentService _paymentService = Substitute.For<IPaymentService>();
private readonly IStripePaymentService _paymentService = Substitute.For<IStripePaymentService>();
private readonly IPremiumUserBillingService _premiumUserBillingService =
Substitute.For<IPremiumUserBillingService>();

View File

@@ -28,7 +28,7 @@ public class PayPalControllerTests(ITestOutputHelper testOutputHelper)
private readonly IOptions<BillingSettings> _billingSettings = Substitute.For<IOptions<BillingSettings>>();
private readonly IMailService _mailService = Substitute.For<IMailService>();
private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();
private readonly IPaymentService _paymentService = Substitute.For<IPaymentService>();
private readonly IStripePaymentService _paymentService = Substitute.For<IStripePaymentService>();
private readonly ITransactionRepository _transactionRepository = Substitute.For<ITransactionRepository>();
private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>();
private readonly IProviderRepository _providerRepository = Substitute.For<IProviderRepository>();

View File

@@ -4,8 +4,8 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Services;
using Bit.Core.Repositories;
using Bit.Core.Services;
using NSubstitute;
using Stripe;
using Xunit;
@@ -61,7 +61,7 @@ public class SetupIntentSucceededHandlerTests
// Assert
await _setupIntentCache.DidNotReceiveWithAnyArgs().GetSubscriberIdForSetupIntent(Arg.Any<string>());
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
@@ -86,7 +86,7 @@ public class SetupIntentSucceededHandlerTests
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
@@ -116,7 +116,7 @@ public class SetupIntentSucceededHandlerTests
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.Received(1).PaymentMethodAttachAsync(
await _stripeAdapter.Received(1).AttachPaymentMethodAsync(
"pm_test",
Arg.Is<PaymentMethodAttachOptions>(o => o.Customer == organization.GatewayCustomerId));
@@ -151,7 +151,7 @@ public class SetupIntentSucceededHandlerTests
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.Received(1).PaymentMethodAttachAsync(
await _stripeAdapter.Received(1).AttachPaymentMethodAsync(
"pm_test",
Arg.Is<PaymentMethodAttachOptions>(o => o.Customer == provider.GatewayCustomerId));
@@ -183,7 +183,7 @@ public class SetupIntentSucceededHandlerTests
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
@@ -216,7 +216,7 @@ public class SetupIntentSucceededHandlerTests
await _handler.HandleAsync(_mockEvent);
// Assert
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());

View File

@@ -20,6 +20,8 @@ using Quartz;
using Stripe;
using Xunit;
using Event = Stripe.Event;
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;
namespace Bit.Billing.Test.Services;
@@ -400,6 +402,75 @@ public class SubscriptionUpdatedHandlerTests
var parsedEvent = new Event { Data = new EventData() };
var premiumPlan = new PremiumPlan
{
Name = "Premium",
Available = true,
LegacyYear = null,
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = IStripeEventUtilityService.PremiumPlanId },
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = "storage-plan-personal" }
};
_pricingClient.ListPremiumPlans().Returns(new List<PremiumPlan> { premiumPlan });
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
_stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>())
.Returns(new StripeList<Invoice> { Data = new List<Invoice>() });
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _userService.Received(1)
.DisablePremiumAsync(userId, currentPeriodEnd);
await _stripeFacade.Received(1)
.CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());
await _stripeFacade.Received(1)
.ListInvoices(Arg.Is<InvoiceListOptions>(o =>
o.Status == StripeInvoiceStatus.Open && o.Subscription == subscriptionId));
}
[Fact]
public async Task HandleAsync_IncompleteExpiredUserSubscription_DisablesPremiumAndCancelsSubscription()
{
// Arrange
var userId = Guid.NewGuid();
var subscriptionId = "sub_123";
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
var subscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.IncompleteExpired,
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } },
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = currentPeriodEnd,
Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId }
}
]
}
};
var parsedEvent = new Event { Data = new EventData() };
var premiumPlan = new PremiumPlan
{
Name = "Premium",
Available = true,
LegacyYear = null,
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = IStripeEventUtilityService.PremiumPlanId },
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = "storage-plan-personal" }
};
_pricingClient.ListPremiumPlans().Returns(new List<PremiumPlan> { premiumPlan });
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
@@ -565,7 +636,7 @@ public class SubscriptionUpdatedHandlerTests
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(10),
Plan = new Plan { Id = "2023-enterprise-org-seat-annually" }
Plan = new Stripe.Plan { Id = "2023-enterprise-org-seat-annually" }
}
]
},
@@ -599,7 +670,7 @@ public class SubscriptionUpdatedHandlerTests
{
Data =
[
new SubscriptionItem { Plan = new Plan { Id = "secrets-manager-enterprise-seat-annually" } }
new SubscriptionItem { Plan = new Stripe.Plan { Id = "secrets-manager-enterprise-seat-annually" } }
]
}
})
@@ -933,6 +1004,134 @@ public class SubscriptionUpdatedHandlerTests
return (providerId, newSubscription, provider, parsedEvent);
}
[Fact]
public async Task HandleAsync_IncompleteUserSubscriptionWithOpenInvoice_CancelsSubscriptionAndDisablesPremium()
{
// Arrange
var userId = Guid.NewGuid();
var subscriptionId = "sub_123";
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
var openInvoice = new Invoice
{
Id = "inv_123",
Status = StripeInvoiceStatus.Open
};
var subscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Incomplete,
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } },
LatestInvoice = openInvoice,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = currentPeriodEnd,
Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId }
}
]
}
};
var parsedEvent = new Event { Data = new EventData() };
var premiumPlan = new PremiumPlan
{
Name = "Premium",
Available = true,
LegacyYear = null,
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = IStripeEventUtilityService.PremiumPlanId },
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = "storage-plan-personal" }
};
_pricingClient.ListPremiumPlans().Returns(new List<PremiumPlan> { premiumPlan });
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
_stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>())
.Returns(new StripeList<Invoice> { Data = new List<Invoice> { openInvoice } });
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _userService.Received(1)
.DisablePremiumAsync(userId, currentPeriodEnd);
await _stripeFacade.Received(1)
.CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());
await _stripeFacade.Received(1)
.ListInvoices(Arg.Is<InvoiceListOptions>(o =>
o.Status == StripeInvoiceStatus.Open && o.Subscription == subscriptionId));
await _stripeFacade.Received(1)
.VoidInvoice(openInvoice.Id);
}
[Fact]
public async Task HandleAsync_IncompleteUserSubscriptionWithoutOpenInvoice_DoesNotCancelSubscription()
{
// Arrange
var userId = Guid.NewGuid();
var subscriptionId = "sub_123";
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
var paidInvoice = new Invoice
{
Id = "inv_123",
Status = StripeInvoiceStatus.Paid
};
var subscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Incomplete,
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } },
LatestInvoice = paidInvoice,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = currentPeriodEnd,
Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId }
}
]
}
};
var parsedEvent = new Event { Data = new EventData() };
var premiumPlan = new PremiumPlan
{
Name = "Premium",
Available = true,
LegacyYear = null,
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = IStripeEventUtilityService.PremiumPlanId },
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = "storage-plan-personal" }
};
_pricingClient.ListPremiumPlans().Returns(new List<PremiumPlan> { premiumPlan });
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _userService.DidNotReceive()
.DisablePremiumAsync(Arg.Any<Guid>(), Arg.Any<DateTime?>());
await _stripeFacade.DidNotReceive()
.CancelSubscription(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
await _stripeFacade.DidNotReceive()
.ListInvoices(Arg.Any<InvoiceListOptions>());
}
public static IEnumerable<object[]> GetNonActiveSubscriptions()
{
return new List<object[]>

View File

@@ -1,10 +1,19 @@
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.Services.NoopImplementations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using NSubstitute;
using StackExchange.Redis;
using Xunit;
@@ -32,6 +41,7 @@ public class EventIntegrationServiceCollectionExtensionsTests
// Mock required repository dependencies for commands
_services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationRepository>());
_services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationConfigurationRepository>());
_services.TryAddScoped(_ => Substitute.For<IOrganizationRepository>());
}
@@ -45,6 +55,9 @@ public class EventIntegrationServiceCollectionExtensionsTests
var cache = provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName);
Assert.NotNull(cache);
var validator = provider.GetRequiredService<IOrganizationIntegrationConfigurationValidator>();
Assert.NotNull(validator);
using var scope = provider.CreateScope();
var sp = scope.ServiceProvider;
@@ -52,6 +65,11 @@ public class EventIntegrationServiceCollectionExtensionsTests
Assert.NotNull(sp.GetService<IUpdateOrganizationIntegrationCommand>());
Assert.NotNull(sp.GetService<IDeleteOrganizationIntegrationCommand>());
Assert.NotNull(sp.GetService<IGetOrganizationIntegrationsQuery>());
Assert.NotNull(sp.GetService<ICreateOrganizationIntegrationConfigurationCommand>());
Assert.NotNull(sp.GetService<IUpdateOrganizationIntegrationConfigurationCommand>());
Assert.NotNull(sp.GetService<IDeleteOrganizationIntegrationConfigurationCommand>());
Assert.NotNull(sp.GetService<IGetOrganizationIntegrationConfigurationsQuery>());
}
[Fact]
@@ -61,8 +79,11 @@ public class EventIntegrationServiceCollectionExtensionsTests
var createIntegrationDescriptor = _services.First(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationCommand));
var createConfigDescriptor = _services.First(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand));
Assert.Equal(ServiceLifetime.Scoped, createIntegrationDescriptor.Lifetime);
Assert.Equal(ServiceLifetime.Scoped, createConfigDescriptor.Lifetime);
}
[Fact]
@@ -117,7 +138,7 @@ public class EventIntegrationServiceCollectionExtensionsTests
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
var createConfigCmdDescriptors = _services.Where(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationCommand)).ToList();
s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand)).ToList();
Assert.Single(createConfigCmdDescriptors);
var updateIntegrationCmdDescriptors = _services.Where(s =>
@@ -148,6 +169,690 @@ public class EventIntegrationServiceCollectionExtensionsTests
Assert.Single(createCmdDescriptors);
}
[Fact]
public void AddOrganizationIntegrationConfigurationCommandsQueries_RegistersAllConfigurationServices()
{
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
Assert.Contains(_services, s => s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IUpdateOrganizationIntegrationConfigurationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IDeleteOrganizationIntegrationConfigurationCommand));
Assert.Contains(_services, s => s.ServiceType == typeof(IGetOrganizationIntegrationConfigurationsQuery));
}
[Fact]
public void AddOrganizationIntegrationConfigurationCommandsQueries_MultipleCalls_IsIdempotent()
{
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
_services.AddOrganizationIntegrationConfigurationCommandsQueries();
var createCmdDescriptors = _services.Where(s =>
s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand)).ToList();
Assert.Single(createCmdDescriptors);
}
[Fact]
public void IsRabbitMqEnabled_AllSettingsPresent_ReturnsTrue()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
});
Assert.True(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingHostName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = null,
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingUsername_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = null,
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingPassword_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = null,
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsRabbitMqEnabled_MissingExchangeName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = null
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));
}
[Fact]
public void IsAzureServiceBusEnabled_AllSettingsPresent_ReturnsTrue()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
});
Assert.True(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
}
[Fact]
public void IsAzureServiceBusEnabled_MissingConnectionString_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = null,
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
}
[Fact]
public void IsAzureServiceBusEnabled_MissingTopicName_ReturnsFalse()
{
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = null
});
Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));
}
[Fact]
public void AddSlackService_AllSettingsPresent_RegistersSlackService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:Slack:ClientId"] = "test-client-id",
["GlobalSettings:Slack:ClientSecret"] = "test-client-secret",
["GlobalSettings:Slack:Scopes"] = "test-scopes"
});
services.TryAddSingleton(globalSettings);
services.AddLogging();
services.AddSlackService(globalSettings);
var provider = services.BuildServiceProvider();
var slackService = provider.GetService<ISlackService>();
Assert.NotNull(slackService);
Assert.IsType<SlackService>(slackService);
var httpClientDescriptor = services.FirstOrDefault(s =>
s.ServiceType == typeof(IHttpClientFactory));
Assert.NotNull(httpClientDescriptor);
}
[Fact]
public void AddSlackService_SettingsMissing_RegistersNoopService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:Slack:ClientId"] = null,
["GlobalSettings:Slack:ClientSecret"] = null,
["GlobalSettings:Slack:Scopes"] = null
});
services.AddSlackService(globalSettings);
var provider = services.BuildServiceProvider();
var slackService = provider.GetService<ISlackService>();
Assert.NotNull(slackService);
Assert.IsType<NoopSlackService>(slackService);
}
[Fact]
public void AddTeamsService_AllSettingsPresent_RegistersTeamsServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:Teams:ClientId"] = "test-client-id",
["GlobalSettings:Teams:ClientSecret"] = "test-client-secret",
["GlobalSettings:Teams:Scopes"] = "test-scopes"
});
services.TryAddSingleton(globalSettings);
services.AddLogging();
services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationRepository>());
services.AddTeamsService(globalSettings);
var provider = services.BuildServiceProvider();
var teamsService = provider.GetService<ITeamsService>();
Assert.NotNull(teamsService);
Assert.IsType<TeamsService>(teamsService);
var bot = provider.GetService<IBot>();
Assert.NotNull(bot);
Assert.IsType<TeamsService>(bot);
var adapter = provider.GetService<IBotFrameworkHttpAdapter>();
Assert.NotNull(adapter);
Assert.IsType<BotFrameworkHttpAdapter>(adapter);
var httpClientDescriptor = services.FirstOrDefault(s =>
s.ServiceType == typeof(IHttpClientFactory));
Assert.NotNull(httpClientDescriptor);
}
[Fact]
public void AddTeamsService_SettingsMissing_RegistersNoopService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:Teams:ClientId"] = null,
["GlobalSettings:Teams:ClientSecret"] = null,
["GlobalSettings:Teams:Scopes"] = null
});
services.AddTeamsService(globalSettings);
var provider = services.BuildServiceProvider();
var teamsService = provider.GetService<ITeamsService>();
Assert.NotNull(teamsService);
Assert.IsType<NoopTeamsService>(teamsService);
}
[Fact]
public void AddRabbitMqIntegration_RegistersEventIntegrationHandler()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.AddLogging();
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var provider = services.BuildServiceProvider();
var handler = provider.GetRequiredKeyedService<IEventMessageHandler>(listenerConfig.RoutingKey);
Assert.NotNull(handler);
}
[Fact]
public void AddRabbitMqIntegration_RegistersEventListenerService()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.TryAddSingleton(Substitute.For<IRabbitMqService>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// AddRabbitMqIntegration should register 2 hosted services (Event + Integration listeners)
Assert.Equal(2, afterCount - beforeCount);
}
[Fact]
public void AddRabbitMqIntegration_RegistersIntegrationListenerService()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.TryAddSingleton(Substitute.For<IRabbitMqService>());
services.TryAddSingleton(Substitute.For<IIntegrationHandler<WebhookIntegrationConfigurationDetails>>());
services.TryAddSingleton(TimeProvider.System);
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// AddRabbitMqIntegration should register 2 hosted services (Event + Integration listeners)
Assert.Equal(2, afterCount - beforeCount);
}
[Fact]
public void AddAzureServiceBusIntegration_RegistersEventIntegrationHandler()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.AddLogging();
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var provider = services.BuildServiceProvider();
var handler = provider.GetRequiredKeyedService<IEventMessageHandler>(listenerConfig.RoutingKey);
Assert.NotNull(handler);
}
[Fact]
public void AddAzureServiceBusIntegration_RegistersEventListenerService()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.TryAddSingleton(Substitute.For<IAzureServiceBusService>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// AddAzureServiceBusIntegration should register 2 hosted services (Event + Integration listeners)
Assert.Equal(2, afterCount - beforeCount);
}
[Fact]
public void AddAzureServiceBusIntegration_RegistersIntegrationListenerService()
{
var services = new ServiceCollection();
var listenerConfig = new TestListenerConfiguration();
// Add required dependencies
services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());
services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());
services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());
services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());
services.TryAddSingleton(Substitute.For<IGroupRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationRepository>());
services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());
services.TryAddSingleton(Substitute.For<IAzureServiceBusService>());
services.TryAddSingleton(Substitute.For<IIntegrationHandler<WebhookIntegrationConfigurationDetails>>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// AddAzureServiceBusIntegration should register 2 hosted services (Event + Integration listeners)
Assert.Equal(2, afterCount - beforeCount);
}
[Fact]
public void AddEventIntegrationServices_RegistersCommonServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
services.AddEventIntegrationServices(globalSettings);
// Verify common services are registered
Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationFilterService));
Assert.Contains(services, s => s.ServiceType == typeof(TimeProvider));
// Verify HttpClients for handlers are registered
var httpClientDescriptors = services.Where(s => s.ServiceType == typeof(IHttpClientFactory)).ToList();
Assert.NotEmpty(httpClientDescriptors);
}
[Fact]
public void AddEventIntegrationServices_RegistersIntegrationHandlers()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
services.AddEventIntegrationServices(globalSettings);
// Verify integration handlers are registered
Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<SlackIntegrationConfigurationDetails>));
Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<WebhookIntegrationConfigurationDetails>));
Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<DatadogIntegrationConfigurationDetails>));
Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<TeamsIntegrationConfigurationDetails>));
}
[Fact]
public void AddEventIntegrationServices_RabbitMqEnabled_RegistersRabbitMqListeners()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
});
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddEventIntegrationServices(globalSettings);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// Should register 11 hosted services for RabbitMQ: 1 repository + 5*2 integration listeners (event+integration)
Assert.Equal(11, afterCount - beforeCount);
}
[Fact]
public void AddEventIntegrationServices_AzureServiceBusEnabled_RegistersAzureListeners()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
});
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddEventIntegrationServices(globalSettings);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// Should register 11 hosted services for Azure Service Bus: 1 repository + 5*2 integration listeners (event+integration)
Assert.Equal(11, afterCount - beforeCount);
}
[Fact]
public void AddEventIntegrationServices_BothEnabled_AzureServiceBusTakesPrecedence()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange",
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
});
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddEventIntegrationServices(globalSettings);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// Should register 11 hosted services for Azure Service Bus: 1 repository + 5*2 integration listeners (event+integration)
// NO RabbitMQ services should be enabled because ASB takes precedence
Assert.Equal(11, afterCount - beforeCount);
}
[Fact]
public void AddEventIntegrationServices_NeitherEnabled_RegistersNoListeners()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));
services.AddEventIntegrationServices(globalSettings);
var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));
// Should register no hosted services when neither RabbitMQ nor Azure Service Bus is enabled
Assert.Equal(0, afterCount - beforeCount);
}
[Fact]
public void AddEventWriteServices_AzureServiceBusEnabled_RegistersAzureServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
});
services.AddEventWriteServices(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(AzureServiceBusService));
Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(EventIntegrationEventWriteService));
}
[Fact]
public void AddEventWriteServices_RabbitMqEnabled_RegistersRabbitMqServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
});
services.AddEventWriteServices(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(RabbitMqService));
Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(EventIntegrationEventWriteService));
}
[Fact]
public void AddEventWriteServices_EventsConnectionStringPresent_RegistersAzureQueueService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:Events:ConnectionString"] = "DefaultEndpointsProtocol=https;AccountName=test;AccountKey=test;EndpointSuffix=core.windows.net",
["GlobalSettings:Events:QueueName"] = "event"
});
services.AddEventWriteServices(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(AzureQueueEventWriteService));
}
[Fact]
public void AddEventWriteServices_SelfHosted_RegistersRepositoryService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:SelfHosted"] = "true"
});
services.AddEventWriteServices(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(RepositoryEventWriteService));
}
[Fact]
public void AddEventWriteServices_NothingEnabled_RegistersNoopService()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
services.AddEventWriteServices(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(NoopEventWriteService));
}
[Fact]
public void AddEventWriteServices_AzureTakesPrecedenceOverRabbitMq()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events",
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
});
services.AddEventWriteServices(globalSettings);
// Should use Azure Service Bus, not RabbitMQ
Assert.Contains(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(AzureServiceBusService));
Assert.DoesNotContain(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(RabbitMqService));
}
[Fact]
public void AddAzureServiceBusListeners_AzureServiceBusEnabled_RegistersAzureServiceBusServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test",
["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events"
});
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
services.AddAzureServiceBusListeners(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IAzureServiceBusService));
Assert.Contains(services, s => s.ServiceType == typeof(IEventRepository));
Assert.Contains(services, s => s.ServiceType == typeof(AzureTableStorageEventHandler));
}
[Fact]
public void AddAzureServiceBusListeners_AzureServiceBusDisabled_ReturnsEarly()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
var initialCount = services.Count;
services.AddAzureServiceBusListeners(globalSettings);
var finalCount = services.Count;
Assert.Equal(initialCount, finalCount);
}
[Fact]
public void AddRabbitMqListeners_RabbitMqEnabled_RegistersRabbitMqServices()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>
{
["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost",
["GlobalSettings:EventLogging:RabbitMq:Username"] = "user",
["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass",
["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange"
});
// Add prerequisites
services.TryAddSingleton(globalSettings);
services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());
services.AddLogging();
services.AddRabbitMqListeners(globalSettings);
Assert.Contains(services, s => s.ServiceType == typeof(IRabbitMqService));
Assert.Contains(services, s => s.ServiceType == typeof(EventRepositoryHandler));
}
[Fact]
public void AddRabbitMqListeners_RabbitMqDisabled_ReturnsEarly()
{
var services = new ServiceCollection();
var globalSettings = CreateGlobalSettings([]);
var initialCount = services.Count;
services.AddRabbitMqListeners(globalSettings);
var finalCount = services.Count;
Assert.Equal(initialCount, finalCount);
}
private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data)
{
var config = new ConfigurationBuilder()

View File

@@ -0,0 +1,179 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class CreateOrganizationIntegrationConfigurationCommandTests
{
[Theory, BitAutoData]
public async Task CreateAsync_Success_CreatesConfigurationAndInvalidatesCache(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = EventType.User_LoggedIn;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(configuration)
.Returns(configuration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
configuration.EventType.Value));
// Also verify RemoveByTagAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
Assert.Equal(configuration, result);
}
[Theory, BitAutoData]
public async Task CreateAsync_WildcardSuccess_CreatesConfigurationAndInvalidatesCache(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = null;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(configuration)
.Returns(configuration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
// Also verify RemoveAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
Assert.Equal(configuration, result);
}
[Theory, BitAutoData]
public async Task CreateAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegrationConfiguration configuration)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task CreateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task CreateAsync_ValidationFails_ThrowsBadRequest(
SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(false);
integration.Id = integrationId;
integration.OrganizationId = organizationId;
configuration.OrganizationIntegrationId = integrationId;
configuration.Template = "template";
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
}

View File

@@ -0,0 +1,211 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class DeleteOrganizationIntegrationConfigurationCommandTests
{
[Theory, BitAutoData]
public async Task DeleteAsync_Success_DeletesConfigurationAndInvalidatesCache(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.Id = configurationId;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = EventType.User_LoggedIn;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(configuration);
await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.DeleteAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
configuration.EventType.Value));
// Also verify RemoveByTagAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_WildcardSuccess_DeletesConfigurationAndInvalidatesCache(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
configuration.Id = configurationId;
configuration.OrganizationIntegrationId = integrationId;
configuration.EventType = null;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(configuration);
await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.DeleteAsync(configuration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
// Also verify RemoveAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_ConfigurationDoesNotExist_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns((OrganizationIntegrationConfiguration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_ConfigurationDoesNotBelongToIntegration_ThrowsNotFound(
SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration configuration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
configuration.Id = configurationId;
configuration.OrganizationIntegrationId = Guid.NewGuid(); // Different integration
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(configuration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
}

View File

@@ -0,0 +1,101 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class GetOrganizationIntegrationConfigurationsQueryTests
{
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_Success_ReturnsConfigurations(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
List<OrganizationIntegrationConfiguration> configurations)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(integrationId)
.Returns(configurations);
var result = await sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(integrationId);
Assert.Equal(configurations.Count, result.Count);
}
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_NoConfigurations_ReturnsEmptyList(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(integrationId)
.Returns([]);
var result = await sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId);
Assert.Empty(result);
}
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetManyByIntegrationAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task GetManyByIntegrationAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetManyByIntegrationAsync(Arg.Any<Guid>());
}
}

View File

@@ -0,0 +1,390 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
[SutProviderCustomize]
public class UpdateOrganizationIntegrationConfigurationCommandTests
{
[Theory, BitAutoData]
public async Task UpdateAsync_Success_UpdatesConfigurationAndInvalidatesCache(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
existingConfiguration.EventType.Value));
// Also verify RemoveByTagAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
Assert.Equal(updatedConfiguration, result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_WildcardSuccess_UpdatesConfigurationAndInvalidatesCache(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = null;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.EventType = null;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
// Also verify RemoveAsync was NOT called
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
Assert.Equal(updatedConfiguration, result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_ChangedEventType_UpdatesConfigurationAndInvalidatesCacheForBothTypes(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
integration.Type = IntegrationType.Webhook;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.EventType = EventType.Cipher_Created;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(integrationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(configurationId);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
existingConfiguration.EventType.Value));
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId,
integration.Type,
updatedConfiguration.EventType.Value));
// Verify RemoveByTagAsync was NOT called since both are specific event types
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
Assert.Equal(updatedConfiguration, result);
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegrationConfiguration updatedConfiguration)
{
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns((OrganizationIntegration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = Guid.NewGuid(); // Different organization
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ConfigurationDoesNotExist_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns((OrganizationIntegrationConfiguration)null);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ConfigurationDoesNotBelongToIntegration_ThrowsNotFound(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
integration.Id = integrationId;
integration.OrganizationId = organizationId;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = Guid.NewGuid(); // Different integration
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ValidationFails_ThrowsBadRequest(
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration)
{
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(false);
integration.Id = integrationId;
integration.OrganizationId = organizationId;
existingConfiguration.Id = configurationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.Id = configurationId;
updatedConfiguration.OrganizationIntegrationId = integrationId;
updatedConfiguration.Template = "template";
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId)
.Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(configurationId)
.Returns(existingConfiguration);
await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveByTagAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ChangedFromWildcardToSpecific_InvalidatesAllCaches(
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration,
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider)
{
integration.OrganizationId = organizationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = null; // Wildcard
updatedConfiguration.EventType = EventType.User_LoggedIn; // Specific
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId).Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(existingConfiguration.Id).Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
await sutProvider.Sut.UpdateAsync(organizationId, integrationId, existingConfiguration.Id, updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task UpdateAsync_ChangedFromSpecificToWildcard_InvalidatesAllCaches(
Guid organizationId,
Guid integrationId,
OrganizationIntegration integration,
OrganizationIntegrationConfiguration existingConfiguration,
OrganizationIntegrationConfiguration updatedConfiguration,
SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider)
{
integration.OrganizationId = organizationId;
existingConfiguration.OrganizationIntegrationId = integrationId;
existingConfiguration.EventType = EventType.User_LoggedIn; // Specific
updatedConfiguration.EventType = null; // Wildcard
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integrationId).Returns(integration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(existingConfiguration.Id).Returns(existingConfiguration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()
.ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(true);
await sutProvider.Sut.UpdateAsync(organizationId, integrationId, existingConfiguration.Id, updatedConfiguration);
await sutProvider.GetDependency<IFusionCache>().Received(1)
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId,
integration.Type));
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
.RemoveAsync(Arg.Any<string>());
}
}

View File

@@ -17,4 +17,5 @@ public class TestListenerConfiguration : IIntegrationListenerConfiguration
public int EventPrefetchCount => 0;
public int IntegrationMaxConcurrentCalls => 1;
public int IntegrationPrefetchCount => 0;
public string RoutingKey => IntegrationType.ToRoutingKey();
}

View File

@@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.Import;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@@ -57,7 +58,7 @@ public class ImportOrganizationUsersAndGroupsCommandTests
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(org).Returns(true);
sutProvider.GetDependency<IStripePaymentService>().HasSecretsManagerStandalone(org).Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers);
sutProvider.GetDependency<IOrganizationRepository>().GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(
new OrganizationSeatCounts

View File

@@ -3,11 +3,11 @@ using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
using Bit.Core.AdminConsole.Utilities.Validation;
using Bit.Core.Billing.Services;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks.Plans;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -50,7 +50,7 @@ public class InviteOrganizationUsersValidatorTests
OccupiedSmSeats = 9
};
sutProvider.GetDependency<IPaymentService>()
sutProvider.GetDependency<IStripePaymentService>()
.HasSecretsManagerStandalone(request.InviteOrganization)
.Returns(true);
@@ -96,7 +96,7 @@ public class InviteOrganizationUsersValidatorTests
OccupiedSmSeats = 9
};
sutProvider.GetDependency<IPaymentService>()
sutProvider.GetDependency<IStripePaymentService>()
.HasSecretsManagerStandalone(request.InviteOrganization)
.Returns(true);
@@ -140,7 +140,7 @@ public class InviteOrganizationUsersValidatorTests
OccupiedSmSeats = 4
};
sutProvider.GetDependency<IPaymentService>()
sutProvider.GetDependency<IStripePaymentService>()
.HasSecretsManagerStandalone(request.InviteOrganization)
.Returns(true);

View File

@@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
@@ -172,7 +173,7 @@ public class ResellerClientOrganizationSignUpCommandTests
private static async Task AssertCleanupIsPerformed(SutProvider<ResellerClientOrganizationSignUpCommand> sutProvider)
{
await sutProvider.GetDependency<IPaymentService>()
await sutProvider.GetDependency<IStripePaymentService>()
.Received(1)
.CancelAndRecoverChargesAsync(Arg.Any<Organization>());
await sutProvider.GetDependency<IOrganizationRepository>()

View File

@@ -2,9 +2,9 @@
using Bit.Core.AdminConsole.Models.Data.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Services;
using Bit.Core.Models.StaticStore;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks.Plans;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -28,7 +28,7 @@ public class UpdateOrganizationSubscriptionCommandTests
// Act
await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate);
await sutProvider.GetDependency<IPaymentService>()
await sutProvider.GetDependency<IStripePaymentService>()
.DidNotReceive()
.AdjustSeatsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());
@@ -53,7 +53,7 @@ public class UpdateOrganizationSubscriptionCommandTests
// Act
await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate);
await sutProvider.GetDependency<IPaymentService>()
await sutProvider.GetDependency<IStripePaymentService>()
.Received(1)
.AdjustSeatsAsync(
Arg.Is<Organization>(x => x.Id == organization.Id),
@@ -81,7 +81,7 @@ public class UpdateOrganizationSubscriptionCommandTests
OrganizationSubscriptionUpdate[] subscriptionsToUpdate =
[new() { Organization = organization, Plan = new Enterprise2023Plan(true) }];
sutProvider.GetDependency<IPaymentService>()
sutProvider.GetDependency<IStripePaymentService>()
.AdjustSeatsAsync(
Arg.Is<Organization>(x => x.Id == organization.Id),
Arg.Is<Plan>(x => x.Type == organization.PlanType),
@@ -115,7 +115,7 @@ public class UpdateOrganizationSubscriptionCommandTests
new() { Organization = failedOrganization, Plan = new Enterprise2023Plan(true) }
];
sutProvider.GetDependency<IPaymentService>()
sutProvider.GetDependency<IStripePaymentService>()
.AdjustSeatsAsync(
Arg.Is<Organization>(x => x.Id == failedOrganization.Id),
Arg.Is<Plan>(x => x.Type == failedOrganization.PlanType),
@@ -124,7 +124,7 @@ public class UpdateOrganizationSubscriptionCommandTests
// Act
await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate);
await sutProvider.GetDependency<IPaymentService>()
await sutProvider.GetDependency<IStripePaymentService>()
.Received(1)
.AdjustSeatsAsync(
Arg.Is<Organization>(x => x.Id == successfulOrganization.Id),

View File

@@ -0,0 +1,244 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Services;
public class OrganizationIntegrationConfigurationValidatorTests
{
private readonly OrganizationIntegrationConfigurationValidator _sut = new();
[Fact]
public void ValidateConfiguration_CloudBillingSyncIntegration_ReturnsFalse()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = "{}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.CloudBillingSync, configuration));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ValidateConfiguration_EmptyTemplate_ReturnsFalse(string? template)
{
var config1 = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration(ChannelId: "C12345")),
Template = template
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Slack, config1));
var config2 = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://example.com"))),
Template = template
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, config2));
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void ValidateConfiguration_EmptyNonNullConfiguration_ReturnsFalse(string? config)
{
var config1 = new OrganizationIntegrationConfiguration
{
Configuration = config,
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Hec, config1));
var config2 = new OrganizationIntegrationConfiguration
{
Configuration = config,
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Datadog, config2));
var config3 = new OrganizationIntegrationConfiguration
{
Configuration = config,
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Teams, config3));
}
[Fact]
public void ValidateConfiguration_NullConfiguration_ReturnsTrue()
{
var config1 = new OrganizationIntegrationConfiguration
{
Configuration = null,
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Hec, config1));
var config2 = new OrganizationIntegrationConfiguration
{
Configuration = null,
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Datadog, config2));
var config3 = new OrganizationIntegrationConfiguration
{
Configuration = null,
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Teams, config3));
}
[Fact]
public void ValidateConfiguration_InvalidJsonConfiguration_ReturnsFalse()
{
var config = new OrganizationIntegrationConfiguration
{
Configuration = "{not valid json}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Slack, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Hec, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Datadog, config));
Assert.False(_sut.ValidateConfiguration(IntegrationType.Teams, config));
}
[Fact]
public void ValidateConfiguration_InvalidJsonFilters_ReturnsFalse()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://example.com"))),
Template = "template",
Filters = "{Not valid json}"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_ScimIntegration_ReturnsFalse()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = "{}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(IntegrationType.Scim, configuration));
}
[Fact]
public void ValidateConfiguration_ValidSlackConfiguration_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration(ChannelId: "C12345")),
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Slack, configuration));
}
[Fact]
public void ValidateConfiguration_ValidSlackConfigurationWithFilters_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345")),
Template = "template",
Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
Rules = [
new IntegrationFilterRule()
{
Operation = IntegrationFilterOperation.Equals,
Property = "CollectionId",
Value = Guid.NewGuid()
}
],
Groups = []
})
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Slack, configuration));
}
[Fact]
public void ValidateConfiguration_ValidNoAuthWebhookConfiguration_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"))),
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_ValidWebhookConfiguration_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN")),
Template = "template"
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_ValidWebhookConfigurationWithFilters_ReturnsTrue()
{
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(
Uri: new Uri("https://example.com"),
Scheme: "Bearer",
Token: "AUTH-TOKEN")),
Template = "template",
Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
{
AndOperator = true,
Rules = [
new IntegrationFilterRule()
{
Operation = IntegrationFilterOperation.Equals,
Property = "CollectionId",
Value = Guid.NewGuid()
}
],
Groups = []
})
};
Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));
}
[Fact]
public void ValidateConfiguration_UnknownIntegrationType_ReturnsFalse()
{
var unknownType = (IntegrationType)999;
var configuration = new OrganizationIntegrationConfiguration
{
Configuration = "{}",
Template = "template"
};
Assert.False(_sut.ValidateConfiguration(unknownType, configuration));
}
}

View File

@@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@@ -1142,7 +1143,7 @@ public class OrganizationServiceTests
.GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));
await stripeAdapter
.Received(1)
.CustomerUpdateAsync(
.UpdateCustomerAsync(
Arg.Is<string>(id => id == organization.GatewayCustomerId),
Arg.Is<CustomerUpdateOptions>(options => options.Email == requestOptionsReturned.Email
&& options.Description == requestOptionsReturned.Description
@@ -1182,7 +1183,7 @@ public class OrganizationServiceTests
.GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
await organizationRepository
.Received(1)
.ReplaceAsync(Arg.Is<Organization>(org => org == organization));

View File

@@ -4,7 +4,7 @@ using Bit.Core.Billing.Organizations.Commands;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Test.Billing.Mocks.Plans;
using Microsoft.Extensions.Logging;
using NSubstitute;
@@ -58,7 +58,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 5500
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(purchase, billingAddress);
@@ -68,7 +68,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(55.00m, total);
// Verify the correct Stripe API call for sponsored subscription
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "US" &&
@@ -116,7 +116,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 8250
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(purchase, billingAddress);
@@ -126,7 +126,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(82.50m, total);
// Verify the correct Stripe API call for standalone secrets manager
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "CA" &&
@@ -179,7 +179,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 12200
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(purchase, billingAddress);
@@ -189,7 +189,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(122.00m, total);
// Verify the correct Stripe API call for comprehensive purchase with storage and service accounts
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "GB" &&
@@ -240,7 +240,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 3300
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(purchase, billingAddress);
@@ -250,7 +250,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(33.00m, total);
// Verify the correct Stripe API call for Families tier (non-seat-based plan)
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "US" &&
@@ -292,7 +292,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 2700
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(purchase, billingAddress);
@@ -302,7 +302,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(27.00m, total);
// Verify the correct Stripe API call for business use in non-US country (tax exempt reverse)
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "DE" &&
@@ -345,7 +345,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 12100
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(purchase, billingAddress);
@@ -355,7 +355,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(121.00m, total);
// Verify the correct Stripe API call for Spanish NIF that adds both Spanish NIF and EU VAT tax IDs
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "ES" &&
@@ -405,7 +405,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 1320
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, planChange, billingAddress);
@@ -415,7 +415,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(13.20m, total);
// Verify the correct Stripe API call for free organization upgrade to Teams
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "US" &&
@@ -458,7 +458,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 4400
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, planChange, billingAddress);
@@ -468,7 +468,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(44.00m, total);
// Verify the correct Stripe API call for free organization upgrade to Families (no SM for Families)
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "CA" &&
@@ -522,7 +522,7 @@ public class PreviewOrganizationTaxCommandTests
Customer = new Customer { Discount = null }
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -534,7 +534,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 9900
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, planChange, billingAddress);
@@ -543,7 +543,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(9.00m, tax);
Assert.Equal(99.00m, total);
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "US" &&
@@ -597,7 +597,7 @@ public class PreviewOrganizationTaxCommandTests
Customer = new Customer { Discount = null }
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -609,7 +609,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 13200
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, planChange, billingAddress);
@@ -618,7 +618,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(12.00m, tax);
Assert.Equal(132.00m, total);
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "US" &&
@@ -661,7 +661,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 8800
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, planChange, billingAddress);
@@ -671,7 +671,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(88.00m, total);
// Verify the correct Stripe API call for free organization with SM to Enterprise
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "GB" &&
@@ -730,7 +730,7 @@ public class PreviewOrganizationTaxCommandTests
Customer = new Customer { Discount = null }
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -738,7 +738,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 16500
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, planChange, billingAddress);
@@ -748,7 +748,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(165.00m, total);
// Verify the correct Stripe API call for existing subscription upgrade
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "DE" &&
@@ -814,7 +814,7 @@ public class PreviewOrganizationTaxCommandTests
}
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -822,7 +822,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 6600
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, planChange, billingAddress);
@@ -832,7 +832,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(66.00m, total);
// Verify the correct Stripe API call preserves existing discount
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "US" &&
@@ -876,8 +876,8 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal("Organization does not have a subscription.", badRequest.Response);
// Verify no Stripe API calls were made
await _stripeAdapter.DidNotReceive().InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>());
await _stripeAdapter.DidNotReceive().SubscriptionGetAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
await _stripeAdapter.DidNotReceive().CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>());
await _stripeAdapter.DidNotReceive().GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
}
#endregion
@@ -919,7 +919,7 @@ public class PreviewOrganizationTaxCommandTests
Customer = customer
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -927,7 +927,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 6600
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, update);
@@ -937,7 +937,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(66.00m, total);
// Verify the correct Stripe API call for PM seats only
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "US" &&
@@ -984,7 +984,7 @@ public class PreviewOrganizationTaxCommandTests
Customer = customer
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -992,7 +992,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 13200
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, update);
@@ -1002,7 +1002,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(132.00m, total);
// Verify the correct Stripe API call for PM seats + storage
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "CA" &&
@@ -1051,7 +1051,7 @@ public class PreviewOrganizationTaxCommandTests
Customer = customer
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -1059,7 +1059,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 8800
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, update);
@@ -1069,7 +1069,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(88.00m, total);
// Verify the correct Stripe API call for SM seats only
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "DE" &&
@@ -1119,7 +1119,7 @@ public class PreviewOrganizationTaxCommandTests
Customer = customer
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -1127,7 +1127,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 16500
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, update);
@@ -1137,7 +1137,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(165.00m, total);
// Verify the correct Stripe API call for SM seats + service accounts with tax ID
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "GB" &&
@@ -1200,7 +1200,7 @@ public class PreviewOrganizationTaxCommandTests
Customer = customer
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -1208,7 +1208,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 27500
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, update);
@@ -1218,7 +1218,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(275.00m, total);
// Verify the correct Stripe API call for comprehensive update with discount and Spanish tax ID
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "ES" &&
@@ -1276,7 +1276,7 @@ public class PreviewOrganizationTaxCommandTests
Customer = customer
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -1284,7 +1284,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 5500
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, update);
@@ -1294,7 +1294,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(55.00m, total);
// Verify the correct Stripe API call for Families tier (personal usage, no business tax exemption)
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "AU" &&
@@ -1334,8 +1334,8 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal("Organization does not have a subscription.", badRequest.Response);
// Verify no Stripe API calls were made
await _stripeAdapter.DidNotReceive().InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>());
await _stripeAdapter.DidNotReceive().SubscriptionGetAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
await _stripeAdapter.DidNotReceive().CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>());
await _stripeAdapter.DidNotReceive().GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
}
[Fact]
@@ -1378,7 +1378,7 @@ public class PreviewOrganizationTaxCommandTests
Customer = customer
};
_stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
_stripeAdapter.GetSubscriptionAsync("sub_test123", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);
var invoice = new Invoice
{
@@ -1386,7 +1386,7 @@ public class PreviewOrganizationTaxCommandTests
Total = 3300
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(organization, update);
@@ -1396,7 +1396,7 @@ public class PreviewOrganizationTaxCommandTests
Assert.Equal(33.00m, total);
// Verify only PM seats are included (storage=0 excluded, SM seats=0 so entire SM excluded)
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "US" &&

View File

@@ -8,7 +8,6 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Platform.Installations;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture;
using Bit.Core.Test.Billing.AutoFixture;
using Bit.Test.Common.AutoFixture;
@@ -59,7 +58,7 @@ public class GetCloudOrganizationLicenseQueryTests
{
installation.Enabled = true;
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);
sutProvider.GetDependency<IStripePaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);
@@ -80,7 +79,7 @@ public class GetCloudOrganizationLicenseQueryTests
{
installation.Enabled = true;
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);
sutProvider.GetDependency<IStripePaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
sutProvider.GetDependency<ILicensingService>()
.CreateOrganizationTokenAsync(organization, installationId, subInfo)
@@ -119,7 +118,7 @@ public class GetCloudOrganizationLicenseQueryTests
installation.Enabled = true;
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider);
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(provider).Returns(subInfo);
sutProvider.GetDependency<IStripePaymentService>().GetSubscriptionAsync(provider).Returns(subInfo);
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);

View File

@@ -8,7 +8,6 @@ using Bit.Core.Billing.Organizations.Queries;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@@ -382,7 +381,7 @@ public class GetOrganizationWarningsQueryTests
var dueDate = now.AddDays(-10);
sutProvider.GetDependency<IStripeAdapter>().InvoiceSearchAsync(Arg.Is<InvoiceSearchOptions>(options =>
sutProvider.GetDependency<IStripeAdapter>().SearchInvoiceAsync(Arg.Is<InvoiceSearchOptions>(options =>
options.Query == $"subscription:'{subscriptionId}' status:'open'")).Returns([
new Invoice { DueDate = dueDate, Created = dueDate.AddDays(-30) }
]);
@@ -542,7 +541,7 @@ public class GetOrganizationWarningsQueryTests
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
@@ -583,7 +582,7 @@ public class GetOrganizationWarningsQueryTests
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
@@ -635,7 +634,7 @@ public class GetOrganizationWarningsQueryTests
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
@@ -687,7 +686,7 @@ public class GetOrganizationWarningsQueryTests
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
@@ -739,7 +738,7 @@ public class GetOrganizationWarningsQueryTests
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>
@@ -785,7 +784,7 @@ public class GetOrganizationWarningsQueryTests
.Returns(true);
sutProvider.GetDependency<IStripeAdapter>()
.TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = new List<Registration>

View File

@@ -4,7 +4,6 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Services;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Extensions;
using Microsoft.Extensions.Logging;
using NSubstitute;
@@ -73,7 +72,7 @@ public class UpdateBillingAddressCommandTests
}
};
_stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
_stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Matches(input) &&
options.HasExpansions("subscriptions")
)).Returns(customer);
@@ -84,7 +83,7 @@ public class UpdateBillingAddressCommandTests
var output = result.AsT0;
Assert.Equivalent(input, output);
await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
}
@@ -131,7 +130,7 @@ public class UpdateBillingAddressCommandTests
}
};
_stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
_stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Matches(input) &&
options.HasExpansions("subscriptions")
)).Returns(customer);
@@ -144,7 +143,7 @@ public class UpdateBillingAddressCommandTests
await _subscriberService.Received(1).CreateStripeCustomer(organization);
await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
}
@@ -192,7 +191,7 @@ public class UpdateBillingAddressCommandTests
}
};
_stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
_stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Matches(input) &&
options.HasExpansions("subscriptions", "tax_ids") &&
options.TaxExempt == TaxExempt.None
@@ -204,7 +203,7 @@ public class UpdateBillingAddressCommandTests
var output = result.AsT0;
Assert.Equivalent(input, output);
await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
}
@@ -260,7 +259,7 @@ public class UpdateBillingAddressCommandTests
}
};
_stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
_stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Matches(input) &&
options.HasExpansions("subscriptions", "tax_ids") &&
options.TaxExempt == TaxExempt.None
@@ -272,10 +271,10 @@ public class UpdateBillingAddressCommandTests
var output = result.AsT0;
Assert.Equivalent(input, output);
await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
await _stripeAdapter.Received(1).TaxIdDeleteAsync(customer.Id, "tax_id_123");
await _stripeAdapter.Received(1).DeleteTaxIdAsync(customer.Id, "tax_id_123");
}
[Fact]
@@ -322,7 +321,7 @@ public class UpdateBillingAddressCommandTests
}
};
_stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
_stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Matches(input) &&
options.HasExpansions("subscriptions", "tax_ids") &&
options.TaxExempt == TaxExempt.Reverse
@@ -334,7 +333,7 @@ public class UpdateBillingAddressCommandTests
var output = result.AsT0;
Assert.Equivalent(input, output);
await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
}
@@ -384,14 +383,14 @@ public class UpdateBillingAddressCommandTests
}
};
_stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
_stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Matches(input) &&
options.HasExpansions("subscriptions", "tax_ids") &&
options.TaxExempt == TaxExempt.Reverse
)).Returns(customer);
_stripeAdapter
.TaxIdCreateAsync(customer.Id,
.CreateTaxIdAsync(customer.Id,
Arg.Is<TaxIdCreateOptions>(options => options.Type == TaxIdType.EUVAT))
.Returns(new TaxId { Type = TaxIdType.EUVAT, Value = "ESA12345678" });
@@ -401,10 +400,10 @@ public class UpdateBillingAddressCommandTests
var output = result.AsT0;
Assert.Equivalent(input with { TaxId = new TaxID(TaxIdType.EUVAT, "ESA12345678") }, output);
await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
await _stripeAdapter.Received(1).TaxIdCreateAsync(organization.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(
await _stripeAdapter.Received(1).CreateTaxIdAsync(organization.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(
options => options.Type == TaxIdType.SpanishNIF &&
options.Value == input.TaxId.Value));
}

View File

@@ -4,7 +4,6 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Services;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Test.Billing.Extensions;
using Braintree;
@@ -82,7 +81,7 @@ public class UpdatePaymentMethodCommandTests
Status = "requires_action"
};
_stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
_stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>
options.PaymentMethod == token && options.HasExpansions("data.payment_method"))).Returns([setupIntent]);
var result = await _command.Run(organization,
@@ -144,7 +143,7 @@ public class UpdatePaymentMethodCommandTests
Status = "requires_action"
};
_stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
_stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>
options.PaymentMethod == token && options.HasExpansions("data.payment_method"))).Returns([setupIntent]);
var result = await _command.Run(organization,
@@ -213,7 +212,7 @@ public class UpdatePaymentMethodCommandTests
Status = "requires_action"
};
_stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
_stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>
options.PaymentMethod == token && options.HasExpansions("data.payment_method"))).Returns([setupIntent]);
var result = await _command.Run(organization,
@@ -232,7 +231,7 @@ public class UpdatePaymentMethodCommandTests
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>
await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>
options.Metadata[MetadataKeys.BraintreeCustomerId] == string.Empty &&
options.Metadata[MetadataKeys.RetiredBraintreeCustomerId] == "braintree_customer_id"));
}
@@ -262,7 +261,7 @@ public class UpdatePaymentMethodCommandTests
const string token = "TOKEN";
_stripeAdapter
.PaymentMethodAttachAsync(token,
.AttachPaymentMethodAsync(token,
Arg.Is<PaymentMethodAttachOptions>(options => options.Customer == customer.Id))
.Returns(new PaymentMethod
{
@@ -291,7 +290,7 @@ public class UpdatePaymentMethodCommandTests
Assert.Equal("9999", maskedCard.Last4);
Assert.Equal("01/2028", maskedCard.Expiration);
await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id,
await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,
Arg.Is<CustomerUpdateOptions>(options => options.InvoiceSettings.DefaultPaymentMethod == token));
}
@@ -315,7 +314,7 @@ public class UpdatePaymentMethodCommandTests
const string token = "TOKEN";
_stripeAdapter
.PaymentMethodAttachAsync(token,
.AttachPaymentMethodAsync(token,
Arg.Is<PaymentMethodAttachOptions>(options => options.Customer == customer.Id))
.Returns(new PaymentMethod
{
@@ -344,10 +343,10 @@ public class UpdatePaymentMethodCommandTests
Assert.Equal("9999", maskedCard.Last4);
Assert.Equal("01/2028", maskedCard.Expiration);
await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id,
await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,
Arg.Is<CustomerUpdateOptions>(options => options.InvoiceSettings.DefaultPaymentMethod == token));
await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id,
await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,
Arg.Is<CustomerUpdateOptions>(options => options.Address.Country == "US" && options.Address.PostalCode == "12345"));
}
@@ -468,7 +467,7 @@ public class UpdatePaymentMethodCommandTests
var maskedPayPalAccount = maskedPaymentMethod.AsT2;
Assert.Equal("user@gmail.com", maskedPayPalAccount.Email);
await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id,
await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,
Arg.Is<CustomerUpdateOptions>(options =>
options.Metadata[MetadataKeys.BraintreeCustomerId] == "braintree_customer_id"));
}

View File

@@ -3,7 +3,6 @@ using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Services;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Extensions;
using Braintree;
using Microsoft.Extensions.Logging;
@@ -166,7 +165,7 @@ public class GetPaymentMethodQueryTests
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
_stripeAdapter
.SetupIntentGet("seti_123",
.GetSetupIntentAsync("seti_123",
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method"))).Returns(
new SetupIntent
{

View File

@@ -3,7 +3,6 @@ using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Services;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Extensions;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
@@ -57,7 +56,7 @@ public class HasPaymentMethodQueryTests
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
_stripeAdapter
.SetupIntentGet("seti_123",
.GetSetupIntentAsync("seti_123",
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method")))
.Returns(new SetupIntent
{
@@ -162,7 +161,7 @@ public class HasPaymentMethodQueryTests
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
_stripeAdapter
.SetupIntentGet("seti_123",
.GetSetupIntentAsync("seti_123",
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method")))
.Returns(new SetupIntent
{
@@ -246,7 +245,7 @@ public class HasPaymentMethodQueryTests
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
_stripeAdapter
.SetupIntentGet("seti_123",
.GetSetupIntentAsync("seti_123",
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method")))
.Returns(new SetupIntent
{

View File

@@ -146,11 +146,11 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockSetupIntent = Substitute.For<SetupIntent>();
mockSetupIntent.Id = "seti_123";
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.SetupIntentList(Arg.Any<SetupIntentListOptions>()).Returns(Task.FromResult(new List<SetupIntent> { mockSetupIntent }));
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.ListSetupIntentsAsync(Arg.Any<SetupIntentListOptions>()).Returns(Task.FromResult(new List<SetupIntent> { mockSetupIntent }));
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
// Act
@@ -158,8 +158,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Assert
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _stripeAdapter.Received(1).CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _userService.Received(1).SaveUserAsync(user);
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
}
@@ -200,10 +200,10 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
// Act
@@ -211,8 +211,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Assert
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _stripeAdapter.Received(1).CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _userService.Received(1).SaveUserAsync(user);
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
}
@@ -243,10 +243,10 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns("bt_customer_123");
@@ -255,8 +255,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Assert
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _stripeAdapter.Received(1).CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _subscriberService.Received(1).CreateBraintreeCustomer(user, paymentMethod.Token);
await _userService.Received(1).SaveUserAsync(user);
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
@@ -299,10 +299,10 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
// Act
@@ -356,8 +356,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Mock that the user has a payment method (this is the key difference from the credit purchase case)
_hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
@@ -365,7 +365,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Assert
Assert.True(result.IsT0);
await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());
await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.DidNotReceive().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
await _updatePaymentMethodCommand.DidNotReceive().Run(Arg.Any<User>(), Arg.Any<TokenizedPaymentMethod>(), Arg.Any<BillingAddress>());
}
@@ -415,8 +415,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
_updatePaymentMethodCommand.Run(Arg.Any<User>(), Arg.Any<TokenizedPaymentMethod>(), Arg.Any<BillingAddress>())
.Returns(mockMaskedPaymentMethod);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
@@ -428,9 +428,9 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Verify GetCustomerOrThrow was called after updating payment method
await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());
// Verify no new customer was created
await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.DidNotReceive().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
// Verify subscription was created
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
// Verify user was updated correctly
Assert.True(user.Premium);
await _userService.Received(1).SaveUserAsync(user);
@@ -474,10 +474,10 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns("bt_customer_123");
// Act
@@ -525,10 +525,10 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
// Act
@@ -577,10 +577,10 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns("bt_customer_123");
// Act
@@ -628,13 +628,13 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockInvoice = Substitute.For<Invoice>();
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_stripeAdapter.SetupIntentList(Arg.Any<SetupIntentListOptions>())
_stripeAdapter.ListSetupIntentsAsync(Arg.Any<SetupIntentListOptions>())
.Returns(Task.FromResult(new List<SetupIntent>())); // Empty list - no setup intent found
// Act
@@ -681,8 +681,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
var mockInvoice = Substitute.For<Invoice>();
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.InvoiceUpdateAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
@@ -690,7 +690,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
// Assert
Assert.True(result.IsT0);
await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());
await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.DidNotReceive().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
Assert.True(user.Premium);
Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate);
}
@@ -716,8 +716,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
Assert.True(result.IsT3); // Assuming T3 is the Unhandled result
Assert.IsType<BillingException>(result.AsT3.Exception);
// Verify no customer was created or subscription attempted
await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.DidNotReceive().SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _stripeAdapter.DidNotReceive().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
await _stripeAdapter.DidNotReceive().CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());
}
@@ -767,8 +767,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
]
};
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, additionalStorage);

View File

@@ -1,7 +1,7 @@
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Billing.Pricing;
using Bit.Core.Services;
using Bit.Core.Billing.Services;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Stripe;
@@ -50,7 +50,7 @@ public class PreviewPremiumTaxCommandTests
Total = 3300
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(0, billingAddress);
@@ -59,7 +59,7 @@ public class PreviewPremiumTaxCommandTests
Assert.Equal(3.00m, tax);
Assert.Equal(33.00m, total);
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "US" &&
@@ -84,7 +84,7 @@ public class PreviewPremiumTaxCommandTests
Total = 5500
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(5, billingAddress);
@@ -93,7 +93,7 @@ public class PreviewPremiumTaxCommandTests
Assert.Equal(5.00m, tax);
Assert.Equal(55.00m, total);
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "CA" &&
@@ -120,7 +120,7 @@ public class PreviewPremiumTaxCommandTests
Total = 2750
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(0, billingAddress);
@@ -129,7 +129,7 @@ public class PreviewPremiumTaxCommandTests
Assert.Equal(2.50m, tax);
Assert.Equal(27.50m, total);
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "GB" &&
@@ -154,7 +154,7 @@ public class PreviewPremiumTaxCommandTests
Total = 8800
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(20, billingAddress);
@@ -163,7 +163,7 @@ public class PreviewPremiumTaxCommandTests
Assert.Equal(8.00m, tax);
Assert.Equal(88.00m, total);
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "DE" &&
@@ -190,7 +190,7 @@ public class PreviewPremiumTaxCommandTests
Total = 4950
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(10, billingAddress);
@@ -199,7 +199,7 @@ public class PreviewPremiumTaxCommandTests
Assert.Equal(4.50m, tax);
Assert.Equal(49.50m, total);
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "AU" &&
@@ -226,7 +226,7 @@ public class PreviewPremiumTaxCommandTests
Total = 3000
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(0, billingAddress);
@@ -235,7 +235,7 @@ public class PreviewPremiumTaxCommandTests
Assert.Equal(0.00m, tax);
Assert.Equal(30.00m, total);
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "US" &&
@@ -260,7 +260,7 @@ public class PreviewPremiumTaxCommandTests
Total = 6600
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(-5, billingAddress);
@@ -269,7 +269,7 @@ public class PreviewPremiumTaxCommandTests
Assert.Equal(6.00m, tax);
Assert.Equal(66.00m, total);
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true &&
options.Currency == "usd" &&
options.CustomerDetails.Address.Country == "FR" &&
@@ -295,7 +295,7 @@ public class PreviewPremiumTaxCommandTests
Total = 3123 // $31.23
};
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
_stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);
var result = await _command.Run(0, billingAddress);

View File

@@ -10,7 +10,6 @@ using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -179,7 +178,7 @@ public class OrganizationBillingServiceTests
SubscriptionCreateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionCreateAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.CreateSubscriptionAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.Returns(new Subscription
{
Id = "sub_test123",
@@ -196,7 +195,7 @@ public class OrganizationBillingServiceTests
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
Assert.NotNull(capturedOptions);
Assert.Equal(7, capturedOptions.TrialPeriodDays);
@@ -255,7 +254,7 @@ public class OrganizationBillingServiceTests
SubscriptionCreateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionCreateAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.CreateSubscriptionAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.Returns(new Subscription
{
Id = "sub_test123",
@@ -272,7 +271,7 @@ public class OrganizationBillingServiceTests
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
Assert.NotNull(capturedOptions);
Assert.Equal(0, capturedOptions.TrialPeriodDays);
@@ -329,7 +328,7 @@ public class OrganizationBillingServiceTests
SubscriptionCreateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionCreateAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.CreateSubscriptionAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))
.Returns(new Subscription
{
Id = "sub_test123",
@@ -346,7 +345,7 @@ public class OrganizationBillingServiceTests
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
Assert.NotNull(capturedOptions);
Assert.Equal(7, capturedOptions.TrialPeriodDays);
@@ -364,7 +363,7 @@ public class OrganizationBillingServiceTests
CustomerUpdateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.CustomerUpdateAsync(
.UpdateCustomerAsync(
Arg.Is<string>(id => id == organization.GatewayCustomerId),
Arg.Do<CustomerUpdateOptions>(options => capturedOptions = options))
.Returns(new Customer());
@@ -375,7 +374,7 @@ public class OrganizationBillingServiceTests
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.CustomerUpdateAsync(
.UpdateCustomerAsync(
organization.GatewayCustomerId,
Arg.Any<CustomerUpdateOptions>());
@@ -401,7 +400,7 @@ public class OrganizationBillingServiceTests
CustomerUpdateOptions capturedOptions = null;
sutProvider.GetDependency<IStripeAdapter>()
.CustomerUpdateAsync(
.UpdateCustomerAsync(
Arg.Is<string>(id => id == organization.GatewayCustomerId),
Arg.Do<CustomerUpdateOptions>(options => capturedOptions = options))
.Returns(new Customer());
@@ -412,7 +411,7 @@ public class OrganizationBillingServiceTests
// Assert
await sutProvider.GetDependency<IStripeAdapter>()
.Received(1)
.CustomerUpdateAsync(
.UpdateCustomerAsync(
organization.GatewayCustomerId,
Arg.Any<CustomerUpdateOptions>());
@@ -445,6 +444,6 @@ public class OrganizationBillingServiceTests
await sutProvider.GetDependency<IStripeAdapter>()
.DidNotReceiveWithAnyArgs()
.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
}
}

View File

@@ -1,9 +1,9 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Entities;
using Bit.Core.Models.BitStripe;
using Bit.Core.Repositories;
using Bit.Core.Services;
using NSubstitute;
using Stripe;
using Xunit;
@@ -19,7 +19,7 @@ public class PaymentHistoryServiceTests
var subscriber = new Organization { GatewayCustomerId = "cus_id", GatewaySubscriptionId = "sub_id" };
var invoices = new List<Invoice> { new() { Id = "in_id" } };
var stripeAdapter = Substitute.For<IStripeAdapter>();
stripeAdapter.InvoiceListAsync(Arg.Any<StripeInvoiceListOptions>()).Returns(invoices);
stripeAdapter.ListInvoicesAsync(Arg.Any<StripeInvoiceListOptions>()).Returns(invoices);
var transactionRepository = Substitute.For<ITransactionRepository>();
var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository);
@@ -29,7 +29,7 @@ public class PaymentHistoryServiceTests
// Assert
Assert.NotEmpty(result);
Assert.Single(result);
await stripeAdapter.Received(1).InvoiceListAsync(Arg.Any<StripeInvoiceListOptions>());
await stripeAdapter.Received(1).ListInvoicesAsync(Arg.Any<StripeInvoiceListOptions>());
}
[Fact]

View File

@@ -1,7 +1,8 @@
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@@ -49,7 +50,7 @@ public class StripePaymentServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(
.GetSubscriptionAsync(
subscriber.GatewaySubscriptionId,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
@@ -100,7 +101,7 @@ public class StripePaymentServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(
.GetSubscriptionAsync(
subscriber.GatewaySubscriptionId,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
@@ -159,7 +160,7 @@ public class StripePaymentServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(
.GetSubscriptionAsync(
subscriber.GatewaySubscriptionId,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
@@ -198,7 +199,7 @@ public class StripePaymentServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(
.GetSubscriptionAsync(
subscriber.GatewaySubscriptionId,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
@@ -256,7 +257,7 @@ public class StripePaymentServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(
.GetSubscriptionAsync(
subscriber.GatewaySubscriptionId,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
@@ -295,7 +296,7 @@ public class StripePaymentServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(
.GetSubscriptionAsync(
subscriber.GatewaySubscriptionId,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
@@ -332,7 +333,7 @@ public class StripePaymentServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(
.GetSubscriptionAsync(
subscriber.GatewaySubscriptionId,
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
@@ -367,7 +368,7 @@ public class StripePaymentServiceTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.SubscriptionGetAsync(
.GetSubscriptionAsync(
Arg.Any<string>(),
Arg.Any<SubscriptionGetOptions>())
.Returns(subscription);
@@ -376,7 +377,7 @@ public class StripePaymentServiceTests
await sutProvider.Sut.GetSubscriptionAsync(subscriber);
// Assert - Verify expand options are correct
await stripeAdapter.Received(1).SubscriptionGetAsync(
await stripeAdapter.Received(1).GetSubscriptionAsync(
subscriber.GatewaySubscriptionId,
Arg.Is<SubscriptionGetOptions>(o =>
o.Expand.Contains("customer.discount.coupon.applies_to") &&
@@ -405,6 +406,6 @@ public class StripePaymentServiceTests
// Verify no Stripe API calls were made
await sutProvider.GetDependency<IStripeAdapter>()
.DidNotReceive()
.SubscriptionGetAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
.GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());
}
}

View File

@@ -3,10 +3,10 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -44,7 +44,7 @@ public class SubscriberServiceTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
await ThrowsBillingExceptionAsync(() =>
@@ -52,11 +52,11 @@ public class SubscriberServiceTests
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.SubscriptionCancelAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
.CancelSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
}
[Theory, BitAutoData]
@@ -81,7 +81,7 @@ public class SubscriberServiceTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
@@ -95,12 +95,12 @@ public class SubscriberServiceTests
await stripeAdapter
.Received(1)
.SubscriptionUpdateAsync(subscriptionId, Arg.Is<SubscriptionUpdateOptions>(
.UpdateSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionUpdateOptions>(
options => options.Metadata["cancellingUserId"] == userId.ToString()));
await stripeAdapter
.Received(1)
.SubscriptionCancelAsync(subscriptionId, Arg.Is<SubscriptionCancelOptions>(options =>
.CancelSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionCancelOptions>(options =>
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason));
}
@@ -127,7 +127,7 @@ public class SubscriberServiceTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
@@ -141,11 +141,11 @@ public class SubscriberServiceTests
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
await stripeAdapter
.Received(1)
.SubscriptionCancelAsync(subscriptionId, Arg.Is<SubscriptionCancelOptions>(options =>
.CancelSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionCancelOptions>(options =>
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason));
}
@@ -170,7 +170,7 @@ public class SubscriberServiceTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var offboardingSurveyResponse = new OffboardingSurveyResponse
@@ -184,7 +184,7 @@ public class SubscriberServiceTests
await stripeAdapter
.Received(1)
.SubscriptionUpdateAsync(subscriptionId, Arg.Is<SubscriptionUpdateOptions>(options =>
.UpdateSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionUpdateOptions>(options =>
options.CancelAtPeriodEnd == true &&
options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&
options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason &&
@@ -192,7 +192,7 @@ public class SubscriberServiceTests
await stripeAdapter
.DidNotReceiveWithAnyArgs()
.SubscriptionCancelAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
.CancelSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
}
#endregion
@@ -223,7 +223,7 @@ public class SubscriberServiceTests
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.GetCustomerAsync(organization.GatewayCustomerId)
.ReturnsNull();
var customer = await sutProvider.Sut.GetCustomer(organization);
@@ -237,7 +237,7 @@ public class SubscriberServiceTests
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.GetCustomerAsync(organization.GatewayCustomerId)
.ThrowsAsync<StripeException>();
var customer = await sutProvider.Sut.GetCustomer(organization);
@@ -253,7 +253,7 @@ public class SubscriberServiceTests
var customer = new Customer();
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.GetCustomerAsync(organization.GatewayCustomerId)
.Returns(customer);
var gotCustomer = await sutProvider.Sut.GetCustomer(organization);
@@ -287,7 +287,7 @@ public class SubscriberServiceTests
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.GetCustomerAsync(organization.GatewayCustomerId)
.ReturnsNull();
await ThrowsBillingExceptionAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));
@@ -301,7 +301,7 @@ public class SubscriberServiceTests
var stripeException = new StripeException();
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.GetCustomerAsync(organization.GatewayCustomerId)
.ThrowsAsync(stripeException);
await ThrowsBillingExceptionAsync(
@@ -318,7 +318,7 @@ public class SubscriberServiceTests
var customer = new Customer();
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId)
.GetCustomerAsync(organization.GatewayCustomerId)
.Returns(customer);
var gotCustomer = await sutProvider.Sut.GetCustomerOrThrow(organization);
@@ -351,7 +351,7 @@ public class SubscriberServiceTests
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
@@ -388,7 +388,7 @@ public class SubscriberServiceTests
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
@@ -442,7 +442,7 @@ public class SubscriberServiceTests
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
@@ -478,7 +478,7 @@ public class SubscriberServiceTests
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
@@ -498,7 +498,7 @@ public class SubscriberServiceTests
{
var customer = new Customer { Id = provider.GatewayCustomerId };
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains(
"invoice_settings.default_payment_method")))
@@ -521,7 +521,7 @@ public class SubscriberServiceTests
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id,
sutProvider.GetDependency<IStripeAdapter>().GetSetupIntentAsync(setupIntent.Id,
Arg.Is<SetupIntentGetOptions>(options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
@@ -541,7 +541,7 @@ public class SubscriberServiceTests
DefaultSource = new BankAccount { Status = "verified", BankName = "Chase", Last4 = "9999" }
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains(
"invoice_settings.default_payment_method")))
@@ -564,7 +564,7 @@ public class SubscriberServiceTests
DefaultSource = new Card { Brand = "Visa", Last4 = "9999", ExpMonth = 9, ExpYear = 2028 }
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains(
"invoice_settings.default_payment_method")))
@@ -596,7 +596,7 @@ public class SubscriberServiceTests
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")))
@@ -636,7 +636,7 @@ public class SubscriberServiceTests
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
var subscription = await sutProvider.Sut.GetSubscription(organization);
@@ -650,7 +650,7 @@ public class SubscriberServiceTests
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.ThrowsAsync<StripeException>();
var subscription = await sutProvider.Sut.GetSubscription(organization);
@@ -666,7 +666,7 @@ public class SubscriberServiceTests
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscription(organization);
@@ -698,7 +698,7 @@ public class SubscriberServiceTests
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.ReturnsNull();
await ThrowsBillingExceptionAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));
@@ -712,7 +712,7 @@ public class SubscriberServiceTests
var stripeException = new StripeException();
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.ThrowsAsync(stripeException);
await ThrowsBillingExceptionAsync(
@@ -729,7 +729,7 @@ public class SubscriberServiceTests
var subscription = new Subscription();
sutProvider.GetDependency<IStripeAdapter>()
.SubscriptionGetAsync(organization.GatewaySubscriptionId)
.GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(subscription);
var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(organization);
@@ -760,7 +760,7 @@ public class SubscriberServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (braintreeGateway, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
@@ -795,7 +795,7 @@ public class SubscriberServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
@@ -832,7 +832,7 @@ public class SubscriberServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
@@ -887,7 +887,7 @@ public class SubscriberServiceTests
};
sutProvider.GetDependency<IStripeAdapter>()
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
@@ -946,21 +946,21 @@ public class SubscriberServiceTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
stripeAdapter
.PaymentMethodListAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
.ListPaymentMethodsAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
.Returns(GetPaymentMethodsAsync(new List<PaymentMethod>()));
await sutProvider.Sut.RemovePaymentSource(organization);
await stripeAdapter.Received(1).BankAccountDeleteAsync(stripeCustomer.Id, bankAccountId);
await stripeAdapter.Received(1).DeleteBankAccountAsync(stripeCustomer.Id, bankAccountId);
await stripeAdapter.Received(1).CardDeleteAsync(stripeCustomer.Id, cardId);
await stripeAdapter.Received(1).DeleteCardAsync(stripeCustomer.Id, cardId);
await stripeAdapter.DidNotReceiveWithAnyArgs()
.PaymentMethodDetachAsync(Arg.Any<string>(), Arg.Any<PaymentMethodDetachOptions>());
.DetachPaymentMethodAsync(Arg.Any<string>(), Arg.Any<PaymentMethodDetachOptions>());
}
[Theory, BitAutoData]
@@ -978,11 +978,11 @@ public class SubscriberServiceTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
.Returns(stripeCustomer);
stripeAdapter
.PaymentMethodListAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
.ListPaymentMethodsAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
.Returns(GetPaymentMethodsAsync(new List<PaymentMethod>
{
new ()
@@ -997,15 +997,15 @@ public class SubscriberServiceTests
await sutProvider.Sut.RemovePaymentSource(organization);
await stripeAdapter.DidNotReceiveWithAnyArgs().BankAccountDeleteAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.DidNotReceiveWithAnyArgs().DeleteBankAccountAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.DidNotReceiveWithAnyArgs().CardDeleteAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.DidNotReceiveWithAnyArgs().DeleteCardAsync(Arg.Any<string>(), Arg.Any<string>());
await stripeAdapter.Received(1)
.PaymentMethodDetachAsync(bankAccountId);
.DetachPaymentMethodAsync(bankAccountId);
await stripeAdapter.Received(1)
.PaymentMethodDetachAsync(cardId);
.DetachPaymentMethodAsync(cardId);
}
private static async IAsyncEnumerable<PaymentMethod> GetPaymentMethodsAsync(
@@ -1050,7 +1050,7 @@ public class SubscriberServiceTests
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId)
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer());
await ThrowsBillingExceptionAsync(() =>
@@ -1062,7 +1062,7 @@ public class SubscriberServiceTests
Provider provider,
SutProvider<SubscriberService> sutProvider)
{
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId)
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer());
await ThrowsBillingExceptionAsync(() =>
@@ -1076,10 +1076,10 @@ public class SubscriberServiceTests
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId)
stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer());
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options => options.PaymentMethod == "TOKEN"))
stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options => options.PaymentMethod == "TOKEN"))
.Returns([new SetupIntent(), new SetupIntent()]);
await ThrowsBillingExceptionAsync(() =>
@@ -1093,7 +1093,7 @@ public class SubscriberServiceTests
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerGetAsync(
stripeAdapter.GetCustomerAsync(
provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
.Returns(new Customer
@@ -1107,10 +1107,10 @@ public class SubscriberServiceTests
var matchingSetupIntent = new SetupIntent { Id = "setup_intent_1" };
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options => options.PaymentMethod == "TOKEN"))
stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options => options.PaymentMethod == "TOKEN"))
.Returns([matchingSetupIntent]);
stripeAdapter.CustomerListPaymentMethods(provider.GatewayCustomerId).Returns([
stripeAdapter.ListCustomerPaymentMethodsAsync(provider.GatewayCustomerId).Returns([
new PaymentMethod { Id = "payment_method_1" }
]);
@@ -1119,12 +1119,12 @@ public class SubscriberServiceTests
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_1");
await stripeAdapter.DidNotReceive().SetupIntentCancel(Arg.Any<string>(),
await stripeAdapter.DidNotReceive().CancelSetupIntentAsync(Arg.Any<string>(),
Arg.Any<SetupIntentCancelOptions>());
await stripeAdapter.Received(1).PaymentMethodDetachAsync("payment_method_1");
await stripeAdapter.Received(1).DetachPaymentMethodAsync("payment_method_1");
await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options => options.Metadata[Core.Billing.Utilities.BraintreeCustomerIdKey] == null));
}
@@ -1135,7 +1135,7 @@ public class SubscriberServiceTests
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerGetAsync(
stripeAdapter.GetCustomerAsync(
provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))
)
@@ -1148,22 +1148,22 @@ public class SubscriberServiceTests
}
});
stripeAdapter.CustomerListPaymentMethods(provider.GatewayCustomerId).Returns([
stripeAdapter.ListCustomerPaymentMethodsAsync(provider.GatewayCustomerId).Returns([
new PaymentMethod { Id = "payment_method_1" }
]);
await sutProvider.Sut.UpdatePaymentSource(provider,
new TokenizedPaymentSource(PaymentMethodType.Card, "TOKEN"));
await stripeAdapter.DidNotReceive().SetupIntentCancel(Arg.Any<string>(),
await stripeAdapter.DidNotReceive().CancelSetupIntentAsync(Arg.Any<string>(),
Arg.Any<SetupIntentCancelOptions>());
await stripeAdapter.Received(1).PaymentMethodDetachAsync("payment_method_1");
await stripeAdapter.Received(1).DetachPaymentMethodAsync("payment_method_1");
await stripeAdapter.Received(1).PaymentMethodAttachAsync("TOKEN", Arg.Is<PaymentMethodAttachOptions>(
await stripeAdapter.Received(1).AttachPaymentMethodAsync("TOKEN", Arg.Is<PaymentMethodAttachOptions>(
options => options.Customer == provider.GatewayCustomerId));
await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options =>
options.InvoiceSettings.DefaultPaymentMethod == "TOKEN" &&
options.Metadata[Core.Billing.Utilities.BraintreeCustomerIdKey] == null));
@@ -1176,7 +1176,7 @@ public class SubscriberServiceTests
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId)
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer
{
Id = provider.GatewayCustomerId,
@@ -1202,7 +1202,7 @@ public class SubscriberServiceTests
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId)
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer
{
Id = provider.GatewayCustomerId,
@@ -1240,7 +1240,7 @@ public class SubscriberServiceTests
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(
provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
.Returns(new Customer
@@ -1294,7 +1294,7 @@ public class SubscriberServiceTests
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(
provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
.Returns(new Customer
@@ -1363,7 +1363,7 @@ public class SubscriberServiceTests
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId)
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
.Returns(new Customer
{
Id = provider.GatewayCustomerId
@@ -1395,7 +1395,7 @@ public class SubscriberServiceTests
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
}
[Theory, BitAutoData]
@@ -1405,7 +1405,7 @@ public class SubscriberServiceTests
{
const string braintreeCustomerId = "braintree_customer_id";
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(
provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
.Returns(new Customer
@@ -1442,7 +1442,7 @@ public class SubscriberServiceTests
await sutProvider.Sut.UpdatePaymentSource(provider,
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN"));
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerUpdateAsync(provider.GatewayCustomerId,
await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerUpdateOptions>(
options => options.Metadata[Core.Billing.Utilities.BraintreeCustomerIdKey] == braintreeCustomerId));
}
@@ -1473,7 +1473,7 @@ public class SubscriberServiceTests
var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } };
stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId, Arg.Is<CustomerGetOptions>(
stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("tax_ids"))).Returns(customer);
var taxInformation = new TaxInformation(
@@ -1487,7 +1487,7 @@ public class SubscriberServiceTests
"NY");
sutProvider.GetDependency<IStripeAdapter>()
.CustomerUpdateAsync(
.UpdateCustomerAsync(
Arg.Is<string>(p => p == provider.GatewayCustomerId),
Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Country == "US" &&
@@ -1522,12 +1522,12 @@ public class SubscriberServiceTests
});
var subscription = new Subscription { Items = new StripeList<SubscriptionItem>() };
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(Arg.Any<string>())
sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(Arg.Any<string>())
.Returns(subscription);
await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation);
await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options =>
options.Address.Country == taxInformation.Country &&
options.Address.PostalCode == taxInformation.PostalCode &&
@@ -1536,13 +1536,13 @@ public class SubscriberServiceTests
options.Address.City == taxInformation.City &&
options.Address.State == taxInformation.State));
await stripeAdapter.Received(1).TaxIdDeleteAsync(provider.GatewayCustomerId, "tax_id_1");
await stripeAdapter.Received(1).DeleteTaxIdAsync(provider.GatewayCustomerId, "tax_id_1");
await stripeAdapter.Received(1).TaxIdCreateAsync(provider.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(
await stripeAdapter.Received(1).CreateTaxIdAsync(provider.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(
options => options.Type == "us_ein" &&
options.Value == taxInformation.TaxId));
await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
}
@@ -1555,7 +1555,7 @@ public class SubscriberServiceTests
var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } };
stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId, Arg.Is<CustomerGetOptions>(
stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("tax_ids"))).Returns(customer);
var taxInformation = new TaxInformation(
@@ -1569,7 +1569,7 @@ public class SubscriberServiceTests
"NY");
sutProvider.GetDependency<IStripeAdapter>()
.CustomerUpdateAsync(
.UpdateCustomerAsync(
Arg.Is<string>(p => p == provider.GatewayCustomerId),
Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Country == "CA" &&
@@ -1605,12 +1605,12 @@ public class SubscriberServiceTests
});
var subscription = new Subscription { Items = new StripeList<SubscriptionItem>() };
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(Arg.Any<string>())
sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(Arg.Any<string>())
.Returns(subscription);
await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation);
await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options =>
options.Address.Country == taxInformation.Country &&
options.Address.PostalCode == taxInformation.PostalCode &&
@@ -1619,16 +1619,16 @@ public class SubscriberServiceTests
options.Address.City == taxInformation.City &&
options.Address.State == taxInformation.State));
await stripeAdapter.Received(1).TaxIdDeleteAsync(provider.GatewayCustomerId, "tax_id_1");
await stripeAdapter.Received(1).DeleteTaxIdAsync(provider.GatewayCustomerId, "tax_id_1");
await stripeAdapter.Received(1).TaxIdCreateAsync(provider.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(
await stripeAdapter.Received(1).CreateTaxIdAsync(provider.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(
options => options.Type == "us_ein" &&
options.Value == taxInformation.TaxId));
await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId,
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId,
Arg.Is<CustomerUpdateOptions>(options => options.TaxExempt == StripeConstants.TaxExempt.Reverse));
await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
}
@@ -1655,7 +1655,7 @@ public class SubscriberServiceTests
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.CustomerGetAsync(Arg.Any<string>());
.GetCustomerAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
@@ -1669,7 +1669,7 @@ public class SubscriberServiceTests
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.CustomerGetAsync(Arg.Any<string>());
.GetCustomerAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
@@ -1678,12 +1678,12 @@ public class SubscriberServiceTests
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId).Returns(new Customer());
stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId).Returns(new Customer());
var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);
Assert.True(result);
await stripeAdapter.Received(1).CustomerGetAsync(organization.GatewayCustomerId);
await stripeAdapter.Received(1).GetCustomerAsync(organization.GatewayCustomerId);
}
[Theory, BitAutoData]
@@ -1693,12 +1693,12 @@ public class SubscriberServiceTests
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var stripeException = new StripeException { StripeError = new StripeError { Code = "resource_missing" } };
stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId).Throws(stripeException);
stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId).Throws(stripeException);
var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);
Assert.False(result);
await stripeAdapter.Received(1).CustomerGetAsync(organization.GatewayCustomerId);
await stripeAdapter.Received(1).GetCustomerAsync(organization.GatewayCustomerId);
}
#endregion
@@ -1724,7 +1724,7 @@ public class SubscriberServiceTests
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.SubscriptionGetAsync(Arg.Any<string>());
.GetSubscriptionAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
@@ -1738,7 +1738,7 @@ public class SubscriberServiceTests
Assert.True(result);
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.SubscriptionGetAsync(Arg.Any<string>());
.GetSubscriptionAsync(Arg.Any<string>());
}
[Theory, BitAutoData]
@@ -1747,12 +1747,12 @@ public class SubscriberServiceTests
SutProvider<SubscriberService> sutProvider)
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId).Returns(new Subscription());
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(new Subscription());
var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);
Assert.True(result);
await stripeAdapter.Received(1).SubscriptionGetAsync(organization.GatewaySubscriptionId);
await stripeAdapter.Received(1).GetSubscriptionAsync(organization.GatewaySubscriptionId);
}
[Theory, BitAutoData]
@@ -1762,12 +1762,12 @@ public class SubscriberServiceTests
{
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var stripeException = new StripeException { StripeError = new StripeError { Code = "resource_missing" } };
stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId).Throws(stripeException);
stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Throws(stripeException);
var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);
Assert.False(result);
await stripeAdapter.Received(1).SubscriptionGetAsync(organization.GatewaySubscriptionId);
await stripeAdapter.Received(1).GetSubscriptionAsync(organization.GatewaySubscriptionId);
}
#endregion

View File

@@ -6,7 +6,6 @@ using Bit.Core.Billing.Services;
using Bit.Core.Billing.Subscriptions.Commands;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Services;
using NSubstitute;
using Stripe;
using Xunit;
@@ -98,13 +97,13 @@ public class RestartSubscriptionCommandTests
};
_subscriberService.GetSubscription(organization).Returns(existingSubscription);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(organization);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is((SubscriptionCreateOptions options) =>
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is((SubscriptionCreateOptions options) =>
options.AutomaticTax.Enabled == true &&
options.CollectionMethod == CollectionMethod.ChargeAutomatically &&
options.Customer == "cus_123" &&
@@ -154,13 +153,13 @@ public class RestartSubscriptionCommandTests
};
_subscriberService.GetSubscription(provider).Returns(existingSubscription);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(provider);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _providerRepository.Received(1).ReplaceAsync(Arg.Is<Provider>(prov =>
prov.Id == providerId &&
@@ -199,13 +198,13 @@ public class RestartSubscriptionCommandTests
};
_subscriberService.GetSubscription(user).Returns(existingSubscription);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);
var result = await _command.Run(user);
Assert.True(result.IsT0);
await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
await _userRepository.Received(1).ReplaceAsync(Arg.Is<User>(u =>
u.Id == userId &&

View File

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -12,7 +13,7 @@ public abstract class CancelSponsorshipCommandTestsBase : FamiliesForEnterpriseT
protected async Task AssertRemovedSponsoredPaymentAsync<T>(Organization sponsoredOrg,
OrganizationSponsorship sponsorship, SutProvider<T> sutProvider)
{
await sutProvider.GetDependency<IPaymentService>().Received(1)
await sutProvider.GetDependency<IStripePaymentService>().Received(1)
.RemoveOrganizationSponsorshipAsync(sponsoredOrg, sponsorship);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).UpsertAsync(sponsoredOrg);
if (sponsorship != null)
@@ -46,7 +47,7 @@ OrganizationSponsorship sponsorship, SutProvider<T> sutProvider)
protected static async Task AssertDidNotRemoveSponsoredPaymentAsync<T>(SutProvider<T> sutProvider)
{
await sutProvider.GetDependency<IPaymentService>().DidNotReceiveWithAnyArgs()
await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()
.RemoveOrganizationSponsorshipAsync(default, default);
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs()
.UpsertAsync(default);

View File

@@ -1,10 +1,10 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -82,7 +82,7 @@ public class SetUpSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
private static async Task AssertDidNotSetUpAsync(SutProvider<SetUpSponsorshipCommand> sutProvider)
{
await sutProvider.GetDependency<IPaymentService>()
await sutProvider.GetDependency<IStripePaymentService>()
.DidNotReceiveWithAnyArgs()
.SponsorOrganizationAsync(default, default);
await sutProvider.GetDependency<IOrganizationRepository>()

View File

@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
@@ -54,7 +55,7 @@ public class AddSecretsManagerSubscriptionCommandTests
c.AdditionalServiceAccounts == additionalServiceAccounts &&
c.AdditionalSeats == organization.Seats.GetValueOrDefault()));
await sutProvider.GetDependency<IPaymentService>().Received()
await sutProvider.GetDependency<IStripePaymentService>().Received()
.AddSecretsManagerToSubscription(organization, plan, additionalSmSeats, additionalServiceAccounts);
// TODO: call ReferenceEventService - see AC-1481
@@ -150,7 +151,7 @@ public class AddSecretsManagerSubscriptionCommandTests
private static async Task VerifyDependencyNotCalledAsync(SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider)
{
await sutProvider.GetDependency<IPaymentService>().DidNotReceive()
await sutProvider.GetDependency<IStripePaymentService>().DidNotReceive()
.AddSecretsManagerToSubscription(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>(), Arg.Any<int>());
// TODO: call ReferenceEventService - see AC-1481

View File

@@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
@@ -86,9 +87,9 @@ public class UpdateSecretsManagerSubscriptionCommandTests
await sutProvider.Sut.UpdateSubscriptionAsync(update);
await sutProvider.GetDependency<IPaymentService>().Received(1)
await sutProvider.GetDependency<IStripePaymentService>().Received(1)
.AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase);
await sutProvider.GetDependency<IPaymentService>().Received(1)
await sutProvider.GetDependency<IStripePaymentService>().Received(1)
.AdjustServiceAccountsAsync(organization, plan, update.SmServiceAccountsExcludingBase);
// TODO: call ReferenceEventService - see AC-1481
@@ -136,9 +137,9 @@ public class UpdateSecretsManagerSubscriptionCommandTests
await sutProvider.Sut.UpdateSubscriptionAsync(update);
await sutProvider.GetDependency<IPaymentService>().Received(1)
await sutProvider.GetDependency<IStripePaymentService>().Received(1)
.AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase);
await sutProvider.GetDependency<IPaymentService>().Received(1)
await sutProvider.GetDependency<IStripePaymentService>().Received(1)
.AdjustServiceAccountsAsync(organization, plan, update.SmServiceAccountsExcludingBase);
// TODO: call ReferenceEventService - see AC-1481
@@ -258,7 +259,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
await sutProvider.Sut.UpdateSubscriptionAsync(update);
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustServiceAccountsAsync(
await sutProvider.GetDependency<IStripePaymentService>().Received(1).AdjustServiceAccountsAsync(
Arg.Is<Organization>(o => o.Id == organizationId),
plan,
expectedSmServiceAccountsExcludingBase);
@@ -779,9 +780,9 @@ public class UpdateSecretsManagerSubscriptionCommandTests
private static async Task VerifyDependencyNotCalledAsync(SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
await sutProvider.GetDependency<IPaymentService>().DidNotReceive()
await sutProvider.GetDependency<IStripePaymentService>().DidNotReceive()
.AdjustSmSeatsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());
await sutProvider.GetDependency<IPaymentService>().DidNotReceive()
await sutProvider.GetDependency<IStripePaymentService>().DidNotReceive()
.AdjustServiceAccountsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());
// TODO: call ReferenceEventService - see AC-1481
await sutProvider.GetDependency<IMailService>().DidNotReceive()

View File

@@ -1,5 +1,6 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@@ -121,7 +122,7 @@ public class UpgradeOrganizationPlanCommandTests
Users = 1
});
await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade);
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSubscription(
await sutProvider.GetDependency<IStripePaymentService>().Received(1).AdjustSubscription(
organization,
MockPlans.Get(planType),
organizationUpgrade.AdditionalSeats,

View File

@@ -55,20 +55,6 @@ public class EventIntegrationsCacheConstantsTests
Assert.NotEqual(keyWithEvent, keyWithDifferentIntegration);
Assert.NotEqual(keyWithEvent, keyWithDifferentOrganization);
Assert.Equal(keyWithEvent, keyWithSameDetails);
var expectedWithNullEvent = $"OrganizationIntegrationConfigurationDetails:{orgId:N}:Hec:";
var keyWithNullEvent = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
orgId, integrationType, null);
var keyWithNullEventDifferentIntegration = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
orgId, IntegrationType.Webhook, null);
var keyWithNullEventDifferentOrganization = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
Guid.NewGuid(), integrationType, null);
Assert.Equal(expectedWithNullEvent, keyWithNullEvent);
Assert.NotEqual(keyWithEvent, keyWithNullEvent);
Assert.NotEqual(keyWithNullEvent, keyWithDifferentEvent);
Assert.NotEqual(keyWithNullEvent, keyWithNullEventDifferentIntegration);
Assert.NotEqual(keyWithNullEvent, keyWithNullEventDifferentOrganization);
}
[Theory, BitAutoData]

View File

@@ -112,13 +112,6 @@ public class BaseRequestValidatorTests
.Returns(true);
}
private void SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(bool recoveryCodeSupportEnabled)
{
_featureService
.IsEnabled(FeatureFlagKeys.RecoveryCodeSupportForSsoRequiredUsers)
.Returns(recoveryCodeSupportEnabled);
}
/* Logic path
* ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
* |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
@@ -126,16 +119,14 @@ public class BaseRequestValidatorTests
* |-> SetErrorResult
*/
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_ContextNotValid_SelfHosted_ShouldBuildErrorResult_ShouldLogWarning(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
_globalSettings.SelfHosted = true;
_sut.isValid = false;
@@ -152,16 +143,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_DeviceNotValidated_ShouldLogError(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
// 1 -> to pass
@@ -194,16 +183,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_DeviceValidated_ShouldSucceed(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
// 1 -> to pass
@@ -240,16 +227,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_ValidatedAuthRequest_ConsumedOnSuccess(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
// 1 -> to pass
@@ -305,16 +290,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_ValidatedAuthRequest_NotConsumed_When2faRequired(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
// 1 -> to pass
@@ -337,7 +320,8 @@ public class BaseRequestValidatorTests
// 2 -> will result to false with no extra configuration
// 3 -> set two factor to be required
requestContext.User.TwoFactorProviders = "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}";
requestContext.User.TwoFactorProviders =
"{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}";
_twoFactorAuthenticationValidator
.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
@@ -347,7 +331,7 @@ public class BaseRequestValidatorTests
.Returns(Task.FromResult(new Dictionary<string, object>
{
{ "TwoFactorProviders", new[] { "0", "1" } },
{ "TwoFactorProviders2", new Dictionary<string, object>{{"Email", null}} }
{ "TwoFactorProviders2", new Dictionary<string, object> { { "Email", null } } }
}));
// Act
@@ -364,16 +348,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_TwoFactorTokenInvalid_ShouldSendFailedTwoFactorEmail(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
var user = requestContext.User;
@@ -408,16 +390,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_TwoFactorRememberTokenExpired_ShouldNotSendFailedTwoFactorEmail(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
var user = requestContext.User;
@@ -463,21 +443,17 @@ public class BaseRequestValidatorTests
// Test grantTypes that require SSO when a user is in an organization that requires it
[Theory]
[BitAutoData("password", true)]
[BitAutoData("password", false)]
[BitAutoData("webauthn", true)]
[BitAutoData("webauthn", false)]
[BitAutoData("refresh_token", true)]
[BitAutoData("refresh_token", false)]
[BitAutoData("password")]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
public async Task ValidateAsync_GrantTypes_OrgSsoRequiredTrue_ShouldSetSsoResult(
string grantType,
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
@@ -497,21 +473,17 @@ public class BaseRequestValidatorTests
// Test grantTypes with RequireSsoPolicyRequirement when feature flag is enabled
[Theory]
[BitAutoData("password", true)]
[BitAutoData("password", false)]
[BitAutoData("webauthn", true)]
[BitAutoData("webauthn", false)]
[BitAutoData("refresh_token", true)]
[BitAutoData("refresh_token", false)]
[BitAutoData("password")]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
public async Task ValidateAsync_GrantTypes_WithPolicyRequirementsEnabled_OrgSsoRequiredTrue_ShouldSetSsoResult(
string grantType,
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
@@ -533,21 +505,17 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData("password", true)]
[BitAutoData("password", false)]
[BitAutoData("webauthn", true)]
[BitAutoData("webauthn", false)]
[BitAutoData("refresh_token", true)]
[BitAutoData("refresh_token", false)]
[BitAutoData("password")]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
public async Task ValidateAsync_GrantTypes_WithPolicyRequirementsEnabled_OrgSsoRequiredFalse_ShouldSucceed(
string grantType,
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
@@ -582,21 +550,17 @@ public class BaseRequestValidatorTests
// Test grantTypes where SSO would be required but the user is not in an
// organization that requires it
[Theory]
[BitAutoData("password", true)]
[BitAutoData("password", false)]
[BitAutoData("webauthn", true)]
[BitAutoData("webauthn", false)]
[BitAutoData("refresh_token", true)]
[BitAutoData("refresh_token", false)]
[BitAutoData("password")]
[BitAutoData("webauthn")]
[BitAutoData("refresh_token")]
public async Task ValidateAsync_GrantTypes_OrgSsoRequiredFalse_ShouldSucceed(
string grantType,
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
@@ -631,19 +595,17 @@ public class BaseRequestValidatorTests
// Test the grantTypes where SSO is in progress or not relevant
[Theory]
[BitAutoData("authorization_code", true)]
[BitAutoData("authorization_code", false)]
[BitAutoData("client_credentials", true)]
[BitAutoData("client_credentials", false)]
[BitAutoData("authorization_code")]
[BitAutoData("client_credentials")]
[BitAutoData("client_credentials")]
public async Task ValidateAsync_GrantTypes_SsoRequiredFalse_ShouldSucceed(
string grantType,
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
@@ -679,16 +641,14 @@ public class BaseRequestValidatorTests
* ValidateAsync -> UserService.IsLegacyUser -> FailAuthForLegacyUserAsync
*/
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var context = CreateContext(tokenRequest, requestContext, grantResult);
var user = context.CustomValidatorRequestContext.User;
user.Key = null;
@@ -713,16 +673,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_CustomResponse_NoMasterPassword_ShouldSetUserDecryptionOptions(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
_userDecryptionOptionsBuilder.ForUser(Arg.Any<User>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithDevice(Arg.Any<Device>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithSso(Arg.Any<SsoConfig>()).Returns(_userDecryptionOptionsBuilder);
@@ -763,19 +721,16 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true, KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(false, KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(true, KdfType.Argon2id, 11, 128, 5)]
[BitAutoData(false, KdfType.Argon2id, 11, 128, 5)]
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]
[BitAutoData(KdfType.Argon2id, 11, 128, 5)]
public async Task ValidateAsync_CustomResponse_MasterPassword_ShouldSetUserDecryptionOptions(
bool featureFlagValue,
KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
_userDecryptionOptionsBuilder.ForUser(Arg.Any<User>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithDevice(Arg.Any<Device>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithSso(Arg.Any<SsoConfig>()).Returns(_userDecryptionOptionsBuilder);
@@ -834,16 +789,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_CustomResponse_ShouldIncludeAccountKeys(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var mockAccountKeys = new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
@@ -916,16 +869,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_CustomResponse_AccountKeysQuery_SkippedWhenPrivateKeyIsNull(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
requestContext.User.PrivateKey = null;
var context = CreateContext(tokenRequest, requestContext, grantResult);
@@ -946,16 +897,14 @@ public class BaseRequestValidatorTests
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_CustomResponse_AccountKeysQuery_CalledWithCorrectUser(
bool featureFlagValue,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
var expectedUser = requestContext.User;
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
@@ -995,22 +944,20 @@ public class BaseRequestValidatorTests
/// Tests the core PM-21153 feature: SSO-required users can use recovery codes to disable 2FA,
/// but must then authenticate via SSO with a descriptive message about the recovery.
/// This test validates:
/// 1. Validation order is changed (2FA before SSO) when recovery code is provided
/// 1. Validation order prioritizes 2FA before SSO when recovery code is provided
/// 2. Recovery code successfully validates and sets TwoFactorRecoveryRequested flag
/// 3. SSO validation then fails with recovery-specific message
/// 4. User is NOT logged in (must authenticate via IdP)
/// </summary>
[Theory]
[BitAutoData(true)] // Feature flag ON - new behavior
[BitAutoData(false)] // Feature flag OFF - should fail at SSO before 2FA recovery
[BitAutoData]
public async Task ValidateAsync_RecoveryCodeForSsoRequiredUser_BlocksWithDescriptiveMessage(
bool featureFlagEnabled,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagEnabled);
var context = CreateContext(tokenRequest, requestContext, grantResult);
var user = requestContext.User;
@@ -1023,8 +970,8 @@ public class BaseRequestValidatorTests
// 2. SSO is required (this user is in an org that requires SSO)
_policyService.AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(true));
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(true));
// 3. 2FA is required
_twoFactorAuthenticationValidator
@@ -1048,30 +995,16 @@ public class BaseRequestValidatorTests
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
if (featureFlagEnabled)
{
// NEW BEHAVIOR: Recovery succeeds, then SSO blocks with descriptive message
Assert.Equal(
"Two-factor recovery has been performed. SSO authentication is required.",
errorResponse.Message);
// Recovery succeeds, then SSO blocks with descriptive message
Assert.Equal(
"Two-factor recovery has been performed. SSO authentication is required.",
errorResponse.Message);
// Verify recovery was marked
Assert.True(requestContext.TwoFactorRecoveryRequested,
"TwoFactorRecoveryRequested flag should be set");
}
else
{
// LEGACY BEHAVIOR: SSO blocks BEFORE recovery can happen
Assert.Equal(
"SSO authentication is required.",
errorResponse.Message);
// Verify recovery was marked
Assert.True(requestContext.TwoFactorRecoveryRequested,
"TwoFactorRecoveryRequested flag should be set");
// Recovery never happened because SSO checked first
Assert.False(requestContext.TwoFactorRecoveryRequested,
"TwoFactorRecoveryRequested should be false (SSO blocked first)");
}
// In both cases: User is NOT logged in
// User is NOT logged in
await _eventService.DidNotReceive().LogUserEventAsync(user.Id, EventType.User_LoggedIn);
}
@@ -1086,16 +1019,14 @@ public class BaseRequestValidatorTests
/// 4. NOT be logged in
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_InvalidRecoveryCodeForSsoRequiredUser_FailsAt2FA(
bool featureFlagEnabled,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagEnabled);
var context = CreateContext(tokenRequest, requestContext, grantResult);
var user = requestContext.User;
@@ -1104,8 +1035,8 @@ public class BaseRequestValidatorTests
// 2. SSO is required
_policyService.AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(true));
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(true));
// 3. 2FA is required
_twoFactorAuthenticationValidator
@@ -1129,51 +1060,32 @@ public class BaseRequestValidatorTests
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
if (featureFlagEnabled)
{
// NEW BEHAVIOR: 2FA is checked first (due to recovery code request), fails with 2FA error
Assert.Equal(
"Two-step token is invalid. Try again.",
errorResponse.Message);
// 2FA is checked first (due to recovery code request), fails with 2FA error
Assert.Equal(
"Two-step token is invalid. Try again.",
errorResponse.Message);
// Recovery was attempted but failed - flag should NOT be set
Assert.False(requestContext.TwoFactorRecoveryRequested,
"TwoFactorRecoveryRequested should be false (recovery failed)");
// Recovery was attempted but failed - flag should NOT be set
Assert.False(requestContext.TwoFactorRecoveryRequested,
"TwoFactorRecoveryRequested should be false (recovery failed)");
// Verify failed 2FA email was sent
await _mailService.Received(1).SendFailedTwoFactorAttemptEmailAsync(
user.Email,
TwoFactorProviderType.RecoveryCode,
Arg.Any<DateTime>(),
Arg.Any<string>());
// Verify failed 2FA email was sent
await _mailService.Received(1).SendFailedTwoFactorAttemptEmailAsync(
user.Email,
TwoFactorProviderType.RecoveryCode,
Arg.Any<DateTime>(),
Arg.Any<string>());
// Verify failed login event was logged
await _eventService.Received(1).LogUserEventAsync(user.Id, EventType.User_FailedLogIn2fa);
}
else
{
// LEGACY BEHAVIOR: SSO is checked first, blocks before 2FA
Assert.Equal(
"SSO authentication is required.",
errorResponse.Message);
// Verify failed login event was logged
await _eventService.Received(1).LogUserEventAsync(user.Id, EventType.User_FailedLogIn2fa);
// 2FA validation never happened
await _mailService.DidNotReceive().SendFailedTwoFactorAttemptEmailAsync(
Arg.Any<string>(),
Arg.Any<TwoFactorProviderType>(),
Arg.Any<DateTime>(),
Arg.Any<string>());
}
// In both cases: User is NOT logged in
// User is NOT logged in
await _eventService.DidNotReceive().LogUserEventAsync(user.Id, EventType.User_LoggedIn);
// Verify user failed login count was updated (in new behavior path)
if (featureFlagEnabled)
{
await _userRepository.Received(1).ReplaceAsync(Arg.Is<User>(u =>
u.Id == user.Id && u.FailedLoginCount > 0));
}
await _userRepository.Received(1).ReplaceAsync(Arg.Is<User>(u =>
u.Id == user.Id && u.FailedLoginCount > 0));
}
/// <summary>
@@ -1187,16 +1099,14 @@ public class BaseRequestValidatorTests
/// This is the "happy path" for recovery code usage.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_RecoveryCodeForNonSsoUser_SuccessfulLogin(
bool featureFlagEnabled,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagEnabled);
var context = CreateContext(tokenRequest, requestContext, grantResult);
var user = requestContext.User;
@@ -1205,8 +1115,8 @@ public class BaseRequestValidatorTests
// 2. SSO is NOT required (this is a regular user, not in SSO org)
_policyService.AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(false));
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(false));
// 3. 2FA is required
_twoFactorAuthenticationValidator
@@ -1243,7 +1153,8 @@ public class BaseRequestValidatorTests
await _sut.ValidateAsync(context);
// Assert
Assert.False(context.GrantResult.IsError, "Authentication should succeed for non-SSO user with valid recovery code");
Assert.False(context.GrantResult.IsError,
"Authentication should succeed for non-SSO user with valid recovery code");
// Verify user successfully logged in
await _eventService.Received(1).LogUserEventAsync(user.Id, EventType.User_LoggedIn);
@@ -1252,19 +1163,9 @@ public class BaseRequestValidatorTests
await _userRepository.Received(1).ReplaceAsync(Arg.Is<User>(u =>
u.Id == user.Id && u.FailedLoginCount == 0));
if (featureFlagEnabled)
{
// NEW BEHAVIOR: Recovery flag should be set for audit purposes
Assert.True(requestContext.TwoFactorRecoveryRequested,
"TwoFactorRecoveryRequested flag should be set for audit/logging");
}
else
{
// LEGACY BEHAVIOR: Recovery flag doesn't exist, but login still succeeds
// (SSO check happens before 2FA in legacy, but user is not SSO-required so both pass)
Assert.False(requestContext.TwoFactorRecoveryRequested,
"TwoFactorRecoveryRequested should be false in legacy mode");
}
// Recovery flag should be set for audit purposes
Assert.True(requestContext.TwoFactorRecoveryRequested,
"TwoFactorRecoveryRequested flag should be set for audit/logging");
}
[Theory]
@@ -1308,16 +1209,14 @@ public class BaseRequestValidatorTests
/// is checked using the old PolicyService.AnyPoliciesApplicableToUserAsync approach.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Disabled_UsesLegacySsoValidation(
bool recoveryCodeFeatureEnabled,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(false);
var context = CreateContext(tokenRequest, requestContext, grantResult);
@@ -1327,7 +1226,7 @@ public class BaseRequestValidatorTests
// SSO is required via legacy path
_policyService.AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(true));
// Act
@@ -1352,16 +1251,14 @@ public class BaseRequestValidatorTests
/// instead of the legacy RequireSsoLoginAsync method.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_UsesNewSsoRequestValidator(
bool recoveryCodeFeatureEnabled,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
var context = CreateContext(tokenRequest, requestContext, grantResult);
@@ -1371,9 +1268,9 @@ public class BaseRequestValidatorTests
// Configure SsoRequestValidator to indicate SSO is required
_ssoRequestValidator.ValidateAsync(
Arg.Any<User>(),
Arg.Any<ValidatedTokenRequest>(),
Arg.Any<CustomValidatorRequestContext>())
Arg.Any<User>(),
Arg.Any<ValidatedTokenRequest>(),
Arg.Any<CustomValidatorRequestContext>())
.Returns(Task.FromResult(false)); // false = SSO required
// Set up the ValidationErrorResult that SsoRequestValidator would set
@@ -1410,16 +1307,14 @@ public class BaseRequestValidatorTests
/// authentication continues successfully through the new validation path.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_SsoNotRequired_SuccessfulLogin(
bool recoveryCodeFeatureEnabled,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
var context = CreateContext(tokenRequest, requestContext, grantResult);
@@ -1430,9 +1325,9 @@ public class BaseRequestValidatorTests
// SsoRequestValidator returns true (SSO not required)
_ssoRequestValidator.ValidateAsync(
Arg.Any<User>(),
Arg.Any<ValidatedTokenRequest>(),
Arg.Any<CustomValidatorRequestContext>())
Arg.Any<User>(),
Arg.Any<ValidatedTokenRequest>(),
Arg.Any<CustomValidatorRequestContext>())
.Returns(Task.FromResult(true));
// No 2FA required
@@ -1473,16 +1368,14 @@ public class BaseRequestValidatorTests
/// (e.g., with organization identifier), that custom response is properly propagated to the result.
/// </summary>
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_PropagatesCustomResponse(
bool recoveryCodeFeatureEnabled,
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
_sut.isValid = true;
@@ -1504,9 +1397,9 @@ public class BaseRequestValidatorTests
var context = CreateContext(tokenRequest, requestContext, grantResult);
_ssoRequestValidator.ValidateAsync(
Arg.Any<User>(),
Arg.Any<ValidatedTokenRequest>(),
Arg.Any<CustomValidatorRequestContext>())
Arg.Any<User>(),
Arg.Any<ValidatedTokenRequest>(),
Arg.Any<CustomValidatorRequestContext>())
.Returns(Task.FromResult(false));
// Act
@@ -1516,7 +1409,8 @@ public class BaseRequestValidatorTests
Assert.True(context.GrantResult.IsError);
Assert.NotNull(context.GrantResult.CustomResponse);
Assert.Contains("SsoOrganizationIdentifier", context.CustomValidatorRequestContext.CustomResponse);
Assert.Equal("test-org-identifier", context.CustomValidatorRequestContext.CustomResponse["SsoOrganizationIdentifier"]);
Assert.Equal("test-org-identifier",
context.CustomValidatorRequestContext.CustomResponse["SsoOrganizationIdentifier"]);
}
/// <summary>
@@ -1527,11 +1421,11 @@ public class BaseRequestValidatorTests
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Disabled_RecoveryWithSso_LegacyMessage(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(true);
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(false);
var context = CreateContext(tokenRequest, requestContext, grantResult);
@@ -1552,7 +1446,7 @@ public class BaseRequestValidatorTests
// SSO is required (legacy check)
_policyService.AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(true));
// Act
@@ -1578,11 +1472,11 @@ public class BaseRequestValidatorTests
[BitAutoData]
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_RecoveryWithSso_NewValidatorMessage(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
[AuthFixtures.CustomValidatorRequestContext]
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(true);
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
var context = CreateContext(tokenRequest, requestContext, grantResult);
@@ -1611,13 +1505,16 @@ public class BaseRequestValidatorTests
};
requestContext.CustomResponse = new Dictionary<string, object>
{
{ "ErrorModel", new ErrorResponseModel("Two-factor recovery has been performed. SSO authentication is required.") }
{
"ErrorModel",
new ErrorResponseModel("Two-factor recovery has been performed. SSO authentication is required.")
}
};
_ssoRequestValidator.ValidateAsync(
Arg.Any<User>(),
Arg.Any<ValidatedTokenRequest>(),
Arg.Any<CustomValidatorRequestContext>())
Arg.Any<User>(),
Arg.Any<ValidatedTokenRequest>(),
Arg.Any<CustomValidatorRequestContext>())
.Returns(Task.FromResult(false));
// Act

View File

@@ -227,7 +227,7 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
// Noop StripePaymentService - this could be changed to integrate with our Stripe test account
Replace(services, Substitute.For<IPaymentService>());
Replace(services, Substitute.For<IStripePaymentService>());
Replace(services, Substitute.For<IOrganizationBillingService>());
});