1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 21:23:39 +00:00

Merge remote-tracking branch 'origin/main' into dbops/dbops-31/csv-import

This commit is contained in:
Mark Kincaid
2025-11-10 16:47:03 -08:00
71 changed files with 2153 additions and 573 deletions

View File

@@ -67,7 +67,6 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
{
Policy = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
},
Metadata = new Dictionary<string, object>
@@ -148,7 +147,6 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
{
Policy = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
Data = new Dictionary<string, object>
{
@@ -218,7 +216,6 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
var policyType = PolicyType.MasterPassword;
var request = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
Data = new Dictionary<string, object>
{
@@ -244,7 +241,6 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
var policyType = PolicyType.SendOptions;
var request = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
Data = new Dictionary<string, object>
{
@@ -267,7 +263,6 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
var policyType = PolicyType.ResetPassword;
var request = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
Data = new Dictionary<string, object>
{
@@ -292,7 +287,6 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
{
Policy = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
Data = new Dictionary<string, object>
{
@@ -321,7 +315,6 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
{
Policy = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
Data = new Dictionary<string, object>
{
@@ -347,7 +340,6 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
{
Policy = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
Data = new Dictionary<string, object>
{
@@ -371,7 +363,6 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
var policyType = PolicyType.SingleOrg;
var request = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
Data = null
};
@@ -393,7 +384,6 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
{
Policy = new PolicyRequestModel
{
Type = policyType,
Enabled = true,
Data = null
},

View File

@@ -133,6 +133,29 @@ public class OrganizationIntegrationControllerTests
.DeleteAsync(organizationIntegration);
}
[Theory, BitAutoData]
public async Task PostDeleteAsync_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
organizationIntegration.OrganizationId = organizationId;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
await sutProvider.Sut.PostDeleteAsync(organizationId, organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.DeleteAsync(organizationIntegration);
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationController> sutProvider,

View File

@@ -51,6 +51,36 @@ public class OrganizationIntegrationsConfigurationControllerTests
.DeleteAsync(organizationIntegrationConfiguration);
}
[Theory, BitAutoData]
public async Task PostDeleteAsync_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
OrganizationIntegrationConfiguration organizationIntegrationConfiguration)
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
await sutProvider.Sut.PostDeleteAsync(organizationId, organizationIntegration.Id, organizationIntegrationConfiguration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetByIdAsync(organizationIntegrationConfiguration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.DeleteAsync(organizationIntegrationConfiguration);
}
[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
@@ -199,27 +229,6 @@ public class OrganizationIntegrationsConfigurationControllerTests
.GetManyByIntegrationAsync(organizationIntegration.Id);
}
// [Theory, BitAutoData]
// public async Task GetAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
// SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
// Guid organizationId,
// OrganizationIntegration organizationIntegration)
// {
// organizationIntegration.OrganizationId = organizationId;
// sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
// sutProvider.GetDependency<ICurrentContext>()
// .OrganizationOwner(organizationId)
// .Returns(true);
// sutProvider.GetDependency<IOrganizationIntegrationRepository>()
// .GetByIdAsync(Arg.Any<Guid>())
// .Returns(organizationIntegration);
// sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
// .GetByIdAsync(Arg.Any<Guid>())
// .ReturnsNull();
//
// await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.Empty, Guid.Empty));
// }
//
[Theory, BitAutoData]
public async Task GetAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
@@ -293,15 +302,16 @@ public class OrganizationIntegrationsConfigurationControllerTests
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
Assert.Equal(expected.Id, requestAction.Id);
Assert.Equal(expected.Configuration, requestAction.Configuration);
Assert.Equal(expected.EventType, requestAction.EventType);
Assert.Equal(expected.Template, requestAction.Template);
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse);
Assert.Equal(expected.Id, createResponse.Id);
Assert.Equal(expected.Configuration, createResponse.Configuration);
Assert.Equal(expected.EventType, createResponse.EventType);
Assert.Equal(expected.Filters, createResponse.Filters);
Assert.Equal(expected.Template, createResponse.Template);
}
[Theory, BitAutoData]
@@ -331,15 +341,16 @@ public class OrganizationIntegrationsConfigurationControllerTests
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
Assert.Equal(expected.Id, requestAction.Id);
Assert.Equal(expected.Configuration, requestAction.Configuration);
Assert.Equal(expected.EventType, requestAction.EventType);
Assert.Equal(expected.Template, requestAction.Template);
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse);
Assert.Equal(expected.Id, createResponse.Id);
Assert.Equal(expected.Configuration, createResponse.Configuration);
Assert.Equal(expected.EventType, createResponse.EventType);
Assert.Equal(expected.Filters, createResponse.Filters);
Assert.Equal(expected.Template, createResponse.Template);
}
[Theory, BitAutoData]
@@ -369,15 +380,16 @@ public class OrganizationIntegrationsConfigurationControllerTests
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
.Returns(organizationIntegrationConfiguration);
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
var createResponse = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
Assert.Equal(expected.Id, requestAction.Id);
Assert.Equal(expected.Configuration, requestAction.Configuration);
Assert.Equal(expected.EventType, requestAction.EventType);
Assert.Equal(expected.Template, requestAction.Template);
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse);
Assert.Equal(expected.Id, createResponse.Id);
Assert.Equal(expected.Configuration, createResponse.Configuration);
Assert.Equal(expected.EventType, createResponse.EventType);
Assert.Equal(expected.Filters, createResponse.Filters);
Assert.Equal(expected.Template, createResponse.Template);
}
[Theory, BitAutoData]
@@ -575,7 +587,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
var requestAction = await sutProvider.Sut.UpdateAsync(
var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
@@ -583,11 +595,12 @@ public class OrganizationIntegrationsConfigurationControllerTests
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
Assert.Equal(expected.Id, requestAction.Id);
Assert.Equal(expected.Configuration, requestAction.Configuration);
Assert.Equal(expected.EventType, requestAction.EventType);
Assert.Equal(expected.Template, requestAction.Template);
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse);
Assert.Equal(expected.Id, updateResponse.Id);
Assert.Equal(expected.Configuration, updateResponse.Configuration);
Assert.Equal(expected.EventType, updateResponse.EventType);
Assert.Equal(expected.Filters, updateResponse.Filters);
Assert.Equal(expected.Template, updateResponse.Template);
}
@@ -619,7 +632,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
var requestAction = await sutProvider.Sut.UpdateAsync(
var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
@@ -627,11 +640,12 @@ public class OrganizationIntegrationsConfigurationControllerTests
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
Assert.Equal(expected.Id, requestAction.Id);
Assert.Equal(expected.Configuration, requestAction.Configuration);
Assert.Equal(expected.EventType, requestAction.EventType);
Assert.Equal(expected.Template, requestAction.Template);
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse);
Assert.Equal(expected.Id, updateResponse.Id);
Assert.Equal(expected.Configuration, updateResponse.Configuration);
Assert.Equal(expected.EventType, updateResponse.EventType);
Assert.Equal(expected.Filters, updateResponse.Filters);
Assert.Equal(expected.Template, updateResponse.Template);
}
[Theory, BitAutoData]
@@ -662,7 +676,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfiguration);
var requestAction = await sutProvider.Sut.UpdateAsync(
var updateResponse = await sutProvider.Sut.UpdateAsync(
organizationId,
organizationIntegration.Id,
organizationIntegrationConfiguration.Id,
@@ -670,11 +684,12 @@ public class OrganizationIntegrationsConfigurationControllerTests
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
Assert.Equal(expected.Id, requestAction.Id);
Assert.Equal(expected.Configuration, requestAction.Configuration);
Assert.Equal(expected.EventType, requestAction.EventType);
Assert.Equal(expected.Template, requestAction.Template);
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse);
Assert.Equal(expected.Id, updateResponse.Id);
Assert.Equal(expected.Configuration, updateResponse.Configuration);
Assert.Equal(expected.EventType, updateResponse.EventType);
Assert.Equal(expected.Filters, updateResponse.Filters);
Assert.Equal(expected.Template, updateResponse.Template);
}
[Theory, BitAutoData]

View File

@@ -71,6 +71,26 @@ public class SlackIntegrationControllerTests
await sutProvider.Sut.CreateAsync(string.Empty, state.ToString()));
}
[Theory, BitAutoData]
public async Task CreateAsync_CallbackUrlIsEmpty_ThrowsBadRequest(
SutProvider<SlackIntegrationController> sutProvider,
OrganizationIntegration integration)
{
integration.Type = IntegrationType.Slack;
integration.Configuration = null;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
.Returns((string?)null);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integration.Id)
.Returns(integration);
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));
}
[Theory, BitAutoData]
public async Task CreateAsync_SlackServiceReturnsEmpty_ThrowsBadRequest(
SutProvider<SlackIntegrationController> sutProvider,
@@ -153,6 +173,8 @@ public class SlackIntegrationControllerTests
OrganizationIntegration wrongOrgIntegration)
{
wrongOrgIntegration.Id = integration.Id;
wrongOrgIntegration.Type = IntegrationType.Slack;
wrongOrgIntegration.Configuration = null;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
@@ -304,6 +326,22 @@ public class SlackIntegrationControllerTests
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
}
[Theory, BitAutoData]
public async Task RedirectAsync_CallbackUrlReturnsEmpty_ThrowsBadRequest(
SutProvider<SlackIntegrationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "SlackIntegration_Create"))
.Returns((string?)null);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
}
[Theory, BitAutoData]
public async Task RedirectAsync_SlackServiceReturnsEmpty_ThrowsNotFound(
SutProvider<SlackIntegrationController> sutProvider,

View File

@@ -60,6 +60,26 @@ public class TeamsIntegrationControllerTests
Assert.IsType<CreatedResult>(requestAction);
}
[Theory, BitAutoData]
public async Task CreateAsync_CallbackUrlIsEmpty_ThrowsBadRequest(
SutProvider<TeamsIntegrationController> sutProvider,
OrganizationIntegration integration)
{
integration.Type = IntegrationType.Teams;
integration.Configuration = null;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
.Returns((string?)null);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(integration.Id)
.Returns(integration);
var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));
}
[Theory, BitAutoData]
public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(
SutProvider<TeamsIntegrationController> sutProvider,
@@ -315,6 +335,30 @@ public class TeamsIntegrationControllerTests
sutProvider.GetDependency<ITeamsService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());
}
[Theory, BitAutoData]
public async Task RedirectAsync_CallbackUrlIsEmpty_ThrowsBadRequest(
SutProvider<TeamsIntegrationController> sutProvider,
Guid organizationId,
OrganizationIntegration integration)
{
integration.OrganizationId = organizationId;
integration.Configuration = null;
integration.Type = IntegrationType.Teams;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.Sut.Url
.RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == "TeamsIntegration_Create"))
.Returns((string?)null);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(organizationId)
.Returns([integration]);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
}
[Theory, BitAutoData]
public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest(
SutProvider<TeamsIntegrationController> sutProvider,

View File

@@ -1,14 +1,47 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;
public class OrganizationIntegrationRequestModelTests
{
[Fact]
public void ToOrganizationIntegration_CreatesNewOrganizationIntegration()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Hec,
Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri("http://localhost"), Scheme: "Bearer", Token: "Token"))
};
var organizationId = Guid.NewGuid();
var organizationIntegration = model.ToOrganizationIntegration(organizationId);
Assert.Equal(organizationIntegration.Type, model.Type);
Assert.Equal(organizationIntegration.Configuration, model.Configuration);
Assert.Equal(organizationIntegration.OrganizationId, organizationId);
}
[Theory, BitAutoData]
public void ToOrganizationIntegration_UpdatesExistingOrganizationIntegration(OrganizationIntegration integration)
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Hec,
Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri("http://localhost"), Scheme: "Bearer", Token: "Token"))
};
var organizationIntegration = model.ToOrganizationIntegration(integration);
Assert.Equal(organizationIntegration.Configuration, model.Configuration);
}
[Fact]
public void Validate_CloudBillingSync_ReturnsNotYetSupportedError()
{

View File

@@ -24,11 +24,11 @@ public class SavePolicyRequestTests
currentContext.OrganizationOwner(organizationId).Returns(true);
var testData = new Dictionary<string, object> { { "test", "value" } };
var policyType = PolicyType.TwoFactorAuthentication;
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.TwoFactorAuthentication,
Enabled = true,
Data = testData
},
@@ -36,7 +36,7 @@ public class SavePolicyRequestTests
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
// Assert
Assert.Equal(PolicyType.TwoFactorAuthentication, result.PolicyUpdate.Type);
@@ -63,17 +63,17 @@ public class SavePolicyRequestTests
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(false);
var policyType = PolicyType.SingleOrg;
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.SingleOrg,
Enabled = false
}
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
// Assert
Assert.Null(result.PolicyUpdate.Data);
@@ -93,17 +93,17 @@ public class SavePolicyRequestTests
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var policyType = PolicyType.SingleOrg;
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.SingleOrg,
Enabled = false
}
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
// Assert
Assert.Null(result.PolicyUpdate.Data);
@@ -124,11 +124,11 @@ public class SavePolicyRequestTests
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var policyType = PolicyType.OrganizationDataOwnership;
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.OrganizationDataOwnership,
Enabled = true
},
Metadata = new Dictionary<string, object>
@@ -138,7 +138,7 @@ public class SavePolicyRequestTests
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
// Assert
Assert.IsType<OrganizationModelOwnershipPolicyModel>(result.Metadata);
@@ -156,17 +156,17 @@ public class SavePolicyRequestTests
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var policyType = PolicyType.OrganizationDataOwnership;
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.OrganizationDataOwnership,
Enabled = true
}
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
// Assert
Assert.NotNull(result);
@@ -193,12 +193,11 @@ public class SavePolicyRequestTests
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var policyType = PolicyType.ResetPassword;
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.ResetPassword,
Enabled = true,
Data = _complexData
},
@@ -206,7 +205,7 @@ public class SavePolicyRequestTests
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
// Assert
var deserializedData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(result.PolicyUpdate.Data);
@@ -234,11 +233,11 @@ public class SavePolicyRequestTests
currentContext.UserId.Returns(userId);
currentContext.OrganizationOwner(organizationId).Returns(true);
var policyType = PolicyType.MaximumVaultTimeout;
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.MaximumVaultTimeout,
Enabled = true
},
Metadata = new Dictionary<string, object>
@@ -248,7 +247,7 @@ public class SavePolicyRequestTests
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
// Assert
Assert.NotNull(result);
@@ -266,19 +265,18 @@ public class SavePolicyRequestTests
currentContext.OrganizationOwner(organizationId).Returns(true);
var errorDictionary = BuildErrorDictionary();
var policyType = PolicyType.OrganizationDataOwnership;
var model = new SavePolicyRequest
{
Policy = new PolicyRequestModel
{
Type = PolicyType.OrganizationDataOwnership,
Enabled = true
},
Metadata = errorDictionary
};
// Act
var result = await model.ToSavePolicyModelAsync(organizationId, currentContext);
var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);
// Assert
Assert.NotNull(result);

View File

@@ -487,14 +487,14 @@ public class PoliciesControllerTests
.Returns(policy);
// Act
var result = await sutProvider.Sut.PutVNext(orgId, model);
var result = await sutProvider.Sut.PutVNext(orgId, policy.Type, model);
// Assert
await sutProvider.GetDependency<IVNextSavePolicyCommand>()
.Received(1)
.SaveAsync(Arg.Is<SavePolicyModel>(
m => m.PolicyUpdate.OrganizationId == orgId &&
m.PolicyUpdate.Type == model.Policy.Type &&
m.PolicyUpdate.Type == policy.Type &&
m.PolicyUpdate.Enabled == model.Policy.Enabled &&
m.PerformedBy.UserId == userId &&
m.PerformedBy.IsOrganizationOwnerOrProvider == true));
@@ -534,14 +534,14 @@ public class PoliciesControllerTests
.Returns(policy);
// Act
var result = await sutProvider.Sut.PutVNext(orgId, model);
var result = await sutProvider.Sut.PutVNext(orgId, policy.Type, model);
// Assert
await sutProvider.GetDependency<ISavePolicyCommand>()
.Received(1)
.VNextSaveAsync(Arg.Is<SavePolicyModel>(
m => m.PolicyUpdate.OrganizationId == orgId &&
m.PolicyUpdate.Type == model.Policy.Type &&
m.PolicyUpdate.Type == policy.Type &&
m.PolicyUpdate.Enabled == model.Policy.Enabled &&
m.PerformedBy.UserId == userId &&
m.PerformedBy.IsOrganizationOwnerOrProvider == true));

View File

@@ -268,7 +268,8 @@ public class UpcomingInvoiceHandlerTests
Arg.Is("sub_123"),
Arg.Is<SubscriptionUpdateOptions>(o =>
o.Items[0].Id == priceSubscriptionId &&
o.Items[0].Price == priceId));
o.Items[0].Price == priceId &&
o.ProrationBehavior == "none"));
// Verify the updated invoice email was sent
await _mailer.Received(1).SendEmail(

View File

@@ -20,6 +20,20 @@ public class IntegrationTemplateContextTests
Assert.Equal(expected, sut.EventMessage);
}
[Theory, BitAutoData]
public void DateIso8601_ReturnsIso8601FormattedDate(EventMessage eventMessage)
{
var testDate = new DateTime(2025, 10, 27, 13, 30, 0, DateTimeKind.Utc);
eventMessage.Date = testDate;
var sut = new IntegrationTemplateContext(eventMessage);
var result = sut.DateIso8601;
Assert.Equal("2025-10-27T13:30:00.0000000Z", result);
// Verify it's valid ISO 8601
Assert.True(DateTime.TryParse(result, out _));
}
[Theory, BitAutoData]
public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, User user)
{

View File

@@ -97,6 +97,8 @@ public class ConfirmOrganizationUserCommandTests
[BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Owner)]
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Admin)]
[BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Owner)]
[BitAutoData(PlanType.FamiliesAnnually2025, OrganizationUserType.Admin)]
[BitAutoData(PlanType.FamiliesAnnually2025, OrganizationUserType.Owner)]
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Admin)]
[BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Owner)]
[BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Admin)]

View File

@@ -23,6 +23,7 @@ public class CloudICloudOrganizationSignUpCommandTests
{
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
[BitAutoData(PlanType.FamiliesAnnually2025)]
public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{
signup.Plan = planType;
@@ -65,6 +66,7 @@ public class CloudICloudOrganizationSignUpCommandTests
[Theory]
[BitAutoData(PlanType.FamiliesAnnually)]
[BitAutoData(PlanType.FamiliesAnnually2025)]
public async Task SignUp_AssignsOwnerToDefaultCollection
(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)
{

View File

@@ -38,6 +38,20 @@ public class EventIntegrationEventWriteServiceTests
organizationId: Arg.Is<string>(orgId => eventMessage.OrganizationId.ToString().Equals(orgId)));
}
[Fact]
public async Task CreateManyAsync_EmptyList_DoesNothing()
{
await Subject.CreateManyAsync([]);
await _eventIntegrationPublisher.DidNotReceiveWithAnyArgs().PublishEventAsync(Arg.Any<string>(), Arg.Any<string>());
}
[Fact]
public async Task DisposeAsync_DisposesEventIntegrationPublisher()
{
await Subject.DisposeAsync();
await _eventIntegrationPublisher.Received(1).DisposeAsync();
}
private static bool AssertJsonStringsMatch(EventMessage expected, string body)
{
var actual = JsonSerializer.Deserialize<EventMessage>(body);

View File

@@ -120,6 +120,16 @@ public class EventIntegrationHandlerTests
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_NoOrganizationId_DoesNothing(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
eventMessage.OrganizationId = null;
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)
{

View File

@@ -1,65 +0,0 @@
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class EventRouteServiceTests
{
private readonly IEventWriteService _broadcastEventWriteService = Substitute.For<IEventWriteService>();
private readonly IEventWriteService _storageEventWriteService = Substitute.For<IEventWriteService>();
private readonly IFeatureService _featureService = Substitute.For<IFeatureService>();
private readonly EventRouteService Subject;
public EventRouteServiceTests()
{
Subject = new EventRouteService(_broadcastEventWriteService, _storageEventWriteService, _featureService);
}
[Theory, BitAutoData]
public async Task CreateAsync_FlagDisabled_EventSentToStorageService(EventMessage eventMessage)
{
_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(false);
await Subject.CreateAsync(eventMessage);
await _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<EventMessage>());
await _storageEventWriteService.Received(1).CreateAsync(eventMessage);
}
[Theory, BitAutoData]
public async Task CreateAsync_FlagEnabled_EventSentToBroadcastService(EventMessage eventMessage)
{
_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(true);
await Subject.CreateAsync(eventMessage);
await _broadcastEventWriteService.Received(1).CreateAsync(eventMessage);
await _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<EventMessage>());
}
[Theory, BitAutoData]
public async Task CreateManyAsync_FlagDisabled_EventsSentToStorageService(IEnumerable<EventMessage> eventMessages)
{
_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(false);
await Subject.CreateManyAsync(eventMessages);
await _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any<IEnumerable<EventMessage>>());
await _storageEventWriteService.Received(1).CreateManyAsync(eventMessages);
}
[Theory, BitAutoData]
public async Task CreateManyAsync_FlagEnabled_EventsSentToBroadcastService(IEnumerable<EventMessage> eventMessages)
{
_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(true);
await Subject.CreateManyAsync(eventMessages);
await _broadcastEventWriteService.Received(1).CreateManyAsync(eventMessages);
await _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any<IEnumerable<EventMessage>>());
}
}

View File

@@ -42,6 +42,35 @@ public class IntegrationFilterServiceTests
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_EqualsUserIdString_Matches(EventMessage eventMessage)
{
var userId = Guid.NewGuid();
eventMessage.UserId = userId;
var group = new IntegrationFilterGroup
{
AndOperator = true,
Rules =
[
new()
{
Property = "UserId",
Operation = IntegrationFilterOperation.Equals,
Value = userId.ToString()
}
]
};
var result = _service.EvaluateFilterGroup(group, eventMessage);
Assert.True(result);
var jsonGroup = JsonSerializer.Serialize(group);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_EqualsUserId_DoesNotMatch(EventMessage eventMessage)
{
@@ -281,6 +310,45 @@ public class IntegrationFilterServiceTests
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_NestedGroups_AnyMatch(EventMessage eventMessage)
{
var id = Guid.NewGuid();
var collectionId = Guid.NewGuid();
eventMessage.UserId = id;
eventMessage.CollectionId = collectionId;
var nestedGroup = new IntegrationFilterGroup
{
AndOperator = false,
Rules =
[
new() { Property = "UserId", Operation = IntegrationFilterOperation.Equals, Value = id },
new()
{
Property = "CollectionId",
Operation = IntegrationFilterOperation.In,
Value = new Guid?[] { Guid.NewGuid() }
}
]
};
var topGroup = new IntegrationFilterGroup
{
AndOperator = false,
Groups = [nestedGroup]
};
var result = _service.EvaluateFilterGroup(topGroup, eventMessage);
Assert.True(result);
var jsonGroup = JsonSerializer.Serialize(topGroup);
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
Assert.NotNull(roundtrippedGroup);
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
}
[Theory, BitAutoData]
public void EvaluateFilterGroup_UnknownProperty_ReturnsFalse(EventMessage eventMessage)
{

View File

@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Models.Slack;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -28,6 +29,9 @@ public class SlackIntegrationHandlerTests
var sutProvider = GetSutProvider();
message.Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token);
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(new SlackSendMessageResponse() { Ok = true, Channel = _channelId });
var result = await sutProvider.Sut.HandleAsync(message);
Assert.True(result.Success);
@@ -39,4 +43,97 @@ public class SlackIntegrationHandlerTests
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
);
}
[Theory]
[InlineData("service_unavailable")]
[InlineData("ratelimited")]
[InlineData("rate_limited")]
[InlineData("internal_error")]
[InlineData("message_limit_exceeded")]
public async Task HandleAsync_FailedRetryableRequest_ReturnsFailureWithRetryable(string error)
{
var sutProvider = GetSutProvider();
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
{
Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
MessageId = "MessageId",
RenderedTemplate = "Test Message"
};
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(new SlackSendMessageResponse() { Ok = false, Channel = _channelId, Error = error });
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.True(result.Retryable);
Assert.NotNull(result.FailureReason);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
);
}
[Theory]
[InlineData("access_denied")]
[InlineData("channel_not_found")]
[InlineData("token_expired")]
[InlineData("token_revoked")]
public async Task HandleAsync_FailedNonRetryableRequest_ReturnsNonRetryableFailure(string error)
{
var sutProvider = GetSutProvider();
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
{
Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
MessageId = "MessageId",
RenderedTemplate = "Test Message"
};
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(new SlackSendMessageResponse() { Ok = false, Channel = _channelId, Error = error });
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.False(result.Retryable);
Assert.NotNull(result.FailureReason);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
);
}
[Fact]
public async Task HandleAsync_NullResponse_ReturnsNonRetryableFailure()
{
var sutProvider = GetSutProvider();
var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()
{
Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),
MessageId = "MessageId",
RenderedTemplate = "Test Message"
};
_slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns((SlackSendMessageResponse?)null);
var result = await sutProvider.Sut.HandleAsync(message);
Assert.False(result.Success);
Assert.False(result.Retryable);
Assert.Equal("Slack response was null", result.FailureReason);
Assert.Equal(result.Message, message);
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
);
}
}

View File

@@ -146,6 +146,27 @@ public class SlackServiceTests
Assert.Empty(result);
}
[Fact]
public async Task GetChannelIdAsync_NoChannelFound_ReturnsEmptyResult()
{
var emptyResponse = JsonSerializer.Serialize(
new
{
ok = true,
channels = Array.Empty<string>(),
response_metadata = new { next_cursor = "" }
});
_handler.When(HttpMethod.Get)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(emptyResponse));
var sutProvider = GetSutProvider();
var result = await sutProvider.Sut.GetChannelIdAsync(_token, "general");
Assert.Empty(result);
}
[Fact]
public async Task GetChannelIdAsync_ReturnsCorrectChannelId()
{
@@ -235,6 +256,32 @@ public class SlackServiceTests
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task GetDmChannelByEmailAsync_ApiErrorUnparsableDmResponse_ReturnsEmptyString()
{
var sutProvider = GetSutProvider();
var email = "user@example.com";
var userId = "U12345";
var userResponse = new
{
ok = true,
user = new { id = userId }
};
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));
_handler.When("https://slack.com/api/conversations.open")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent("NOT JSON"));
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task GetDmChannelByEmailAsync_ApiErrorUserResponse_ReturnsEmptyString()
{
@@ -244,7 +291,7 @@ public class SlackServiceTests
var userResponse = new
{
ok = false,
error = "An error occured"
error = "An error occurred"
};
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
@@ -256,6 +303,21 @@ public class SlackServiceTests
Assert.Equal(string.Empty, result);
}
[Fact]
public async Task GetDmChannelByEmailAsync_ApiErrorUnparsableUserResponse_ReturnsEmptyString()
{
var sutProvider = GetSutProvider();
var email = "user@example.com";
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent("Not JSON"));
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
Assert.Equal(string.Empty, result);
}
[Fact]
public void GetRedirectUrl_ReturnsCorrectUrl()
{
@@ -341,18 +403,29 @@ public class SlackServiceTests
}
[Fact]
public async Task SendSlackMessageByChannelId_Sends_Correct_Message()
public async Task SendSlackMessageByChannelId_Success_ReturnsSuccessfulResponse()
{
var sutProvider = GetSutProvider();
var channelId = "C12345";
var message = "Hello, Slack!";
var jsonResponse = JsonSerializer.Serialize(new
{
ok = true,
channel = channelId,
});
_handler.When(HttpMethod.Post)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(string.Empty));
.WithContent(new StringContent(jsonResponse));
await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
// Response was parsed correctly
Assert.NotNull(result);
Assert.True(result.Ok);
// Request was sent correctly
Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0];
Assert.NotNull(request);
@@ -365,4 +438,62 @@ public class SlackServiceTests
Assert.Equal(message, json.RootElement.GetProperty("text").GetString() ?? string.Empty);
Assert.Equal(channelId, json.RootElement.GetProperty("channel").GetString() ?? string.Empty);
}
[Fact]
public async Task SendSlackMessageByChannelId_Failure_ReturnsErrorResponse()
{
var sutProvider = GetSutProvider();
var channelId = "C12345";
var message = "Hello, Slack!";
var jsonResponse = JsonSerializer.Serialize(new
{
ok = false,
channel = channelId,
error = "error"
});
_handler.When(HttpMethod.Post)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent(jsonResponse));
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
// Response was parsed correctly
Assert.NotNull(result);
Assert.False(result.Ok);
Assert.NotNull(result.Error);
}
[Fact]
public async Task SendSlackMessageByChannelIdAsync_InvalidJson_ReturnsNull()
{
var sutProvider = GetSutProvider();
var channelId = "C12345";
var message = "Hello world!";
_handler.When(HttpMethod.Post)
.RespondWith(HttpStatusCode.OK)
.WithContent(new StringContent("Not JSON"));
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
Assert.Null(result);
}
[Fact]
public async Task SendSlackMessageByChannelIdAsync_HttpServerError_ReturnsNull()
{
var sutProvider = GetSutProvider();
var channelId = "C12345";
var message = "Hello world!";
_handler.When(HttpMethod.Post)
.RespondWith(HttpStatusCode.InternalServerError)
.WithContent(new StringContent(string.Empty));
var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
Assert.Null(result);
}
}

View File

@@ -0,0 +1,527 @@
using System.Net;
using Bit.Core.Billing;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
using NSubstitute;
using RichardSzalay.MockHttp;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.Billing.Pricing;
[SutProviderCustomize]
public class PricingClientTests
{
#region GetLookupKey Tests (via GetPlan)
[Fact]
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagEnabled_UsesFamilies2025LookupKey()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
var planJson = CreatePlanJson("families-2025", "Families 2025", "families", 40M, "price_id");
mockHttp.Expect(HttpMethod.Get, "https://test.com/plans/organization/families-2025")
.Respond("application/json", planJson);
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
.Respond("application/json", planJson);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
// Assert
Assert.NotNull(result);
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
mockHttp.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagDisabled_UsesFamiliesLookupKey()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
var planJson = CreatePlanJson("families", "Families", "families", 40M, "price_id");
mockHttp.Expect(HttpMethod.Get, "https://test.com/plans/organization/families")
.Respond("application/json", planJson);
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
.Respond("application/json", planJson);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
// Assert
Assert.NotNull(result);
// PreProcessFamiliesPreMigrationPlan should change "families" to "families-2025" when FF is disabled
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
mockHttp.VerifyNoOutstandingExpectation();
}
#endregion
#region PreProcessFamiliesPreMigrationPlan Tests (via GetPlan)
[Fact]
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagDisabled_ReturnsFamiliesAnnually2025PlanType()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
// billing-pricing returns "families" lookup key because the flag is off
var planJson = CreatePlanJson("families", "Families", "families", 40M, "price_id");
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
.Respond("application/json", planJson);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
// Assert
Assert.NotNull(result);
// PreProcessFamiliesPreMigrationPlan should convert the families lookup key to families-2025
// and the PlanAdapter should assign the correct FamiliesAnnually2025 plan type
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
mockHttp.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagEnabled_ReturnsFamiliesAnnually2025PlanType()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
var planJson = CreatePlanJson("families-2025", "Families", "families", 40M, "price_id");
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
.Respond("application/json", planJson);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
// Assert
Assert.NotNull(result);
// PreProcessFamiliesPreMigrationPlan should ignore the lookup key because the flag is on
// and the PlanAdapter should assign the correct FamiliesAnnually2025 plan type
Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);
mockHttp.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task GetPlan_WithFamiliesAnnuallyAndFeatureFlagEnabled_ReturnsFamiliesAnnuallyPlanType()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
var planJson = CreatePlanJson("families", "Families", "families", 40M, "price_id");
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
.Respond("application/json", planJson);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually);
// Assert
Assert.NotNull(result);
// PreProcessFamiliesPreMigrationPlan should ignore the lookup key because the flag is on
// and the PlanAdapter should assign the correct FamiliesAnnually plan type
Assert.Equal(PlanType.FamiliesAnnually, result.Type);
mockHttp.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task GetPlan_WithOtherLookupKey_KeepsLookupKeyUnchanged()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
var planJson = CreatePlanJson("enterprise-annually", "Enterprise", "enterprise", 144M, "price_id");
mockHttp.Expect(HttpMethod.Get, "https://test.com/plans/organization/enterprise-annually")
.Respond("application/json", planJson);
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
.Respond("application/json", planJson);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act
var result = await pricingClient.GetPlan(PlanType.EnterpriseAnnually);
// Assert
Assert.NotNull(result);
Assert.Equal(PlanType.EnterpriseAnnually, result.Type);
mockHttp.VerifyNoOutstandingExpectation();
}
#endregion
#region ListPlans Tests
[Fact]
public async Task ListPlans_WithFeatureFlagDisabled_ReturnsListWithPreProcessing()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
// biling-pricing would return "families" because the flag is disabled
var plansJson = $@"[
{CreatePlanJson("families", "Families", "families", 40M, "price_id")},
{CreatePlanJson("enterprise-annually", "Enterprise", "enterprise", 144M, "price_id")}
]";
mockHttp.When(HttpMethod.Get, "*/plans/organization")
.Respond("application/json", plansJson);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act
var result = await pricingClient.ListPlans();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count);
// First plan should have been preprocessed from "families" to "families-2025"
Assert.Equal(PlanType.FamiliesAnnually2025, result[0].Type);
// Second plan should remain unchanged
Assert.Equal(PlanType.EnterpriseAnnually, result[1].Type);
mockHttp.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task ListPlans_WithFeatureFlagEnabled_ReturnsListWithoutPreProcessing()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
var plansJson = $@"[
{CreatePlanJson("families", "Families", "families", 40M, "price_id")}
]";
mockHttp.When(HttpMethod.Get, "*/plans/organization")
.Respond("application/json", plansJson);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act
var result = await pricingClient.ListPlans();
// Assert
Assert.NotNull(result);
Assert.Single(result);
// Plan should remain as FamiliesAnnually when FF is enabled
Assert.Equal(PlanType.FamiliesAnnually, result[0].Type);
mockHttp.VerifyNoOutstandingExpectation();
}
#endregion
#region GetPlan - Additional Coverage
[Theory, BitAutoData]
public async Task GetPlan_WhenSelfHosted_ReturnsNull(
SutProvider<PricingClient> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<GlobalSettings>();
globalSettings.SelfHosted = true;
// Act
var result = await sutProvider.Sut.GetPlan(PlanType.FamiliesAnnually2025);
// Assert
Assert.Null(result);
}
[Theory, BitAutoData]
public async Task GetPlan_WhenPricingServiceDisabled_ReturnsStaticStorePlan(
SutProvider<PricingClient> sutProvider)
{
// Arrange
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.UsePricingService)
.Returns(false);
// Act
var result = await sutProvider.Sut.GetPlan(PlanType.FamiliesAnnually);
// Assert
Assert.NotNull(result);
Assert.Equal(PlanType.FamiliesAnnually, result.Type);
}
[Theory, BitAutoData]
public async Task GetPlan_WhenLookupKeyNotFound_ReturnsNull(
SutProvider<PricingClient> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.UsePricingService)
.Returns(true);
// Act - Using PlanType that doesn't have a lookup key mapping
var result = await sutProvider.Sut.GetPlan(unchecked((PlanType)999));
// Assert
Assert.Null(result);
}
[Fact]
public async Task GetPlan_WhenPricingServiceReturnsNotFound_ReturnsNull()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
.Respond(HttpStatusCode.NotFound);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act
var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);
// Assert
Assert.Null(result);
}
[Fact]
public async Task GetPlan_WhenPricingServiceReturnsError_ThrowsBillingException()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
mockHttp.When(HttpMethod.Get, "*/plans/organization/*")
.Respond(HttpStatusCode.InternalServerError);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act & Assert
await Assert.ThrowsAsync<BillingException>(() =>
pricingClient.GetPlan(PlanType.FamiliesAnnually2025));
}
#endregion
#region ListPlans - Additional Coverage
[Theory, BitAutoData]
public async Task ListPlans_WhenSelfHosted_ReturnsEmptyList(
SutProvider<PricingClient> sutProvider)
{
// Arrange
var globalSettings = sutProvider.GetDependency<GlobalSettings>();
globalSettings.SelfHosted = true;
// Act
var result = await sutProvider.Sut.ListPlans();
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
[Theory, BitAutoData]
public async Task ListPlans_WhenPricingServiceDisabled_ReturnsStaticStorePlans(
SutProvider<PricingClient> sutProvider)
{
// Arrange
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.UsePricingService)
.Returns(false);
// Act
var result = await sutProvider.Sut.ListPlans();
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.Equal(StaticStore.Plans.Count(), result.Count);
}
[Fact]
public async Task ListPlans_WhenPricingServiceReturnsError_ThrowsBillingException()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
mockHttp.When(HttpMethod.Get, "*/plans/organization")
.Respond(HttpStatusCode.InternalServerError);
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.UsePricingService).Returns(true);
var globalSettings = new GlobalSettings { SelfHosted = false };
var httpClient = new HttpClient(mockHttp)
{
BaseAddress = new Uri("https://test.com/")
};
var logger = Substitute.For<ILogger<PricingClient>>();
var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);
// Act & Assert
await Assert.ThrowsAsync<BillingException>(() =>
pricingClient.ListPlans());
}
#endregion
private static string CreatePlanJson(
string lookupKey,
string name,
string tier,
decimal seatsPrice,
string seatsStripePriceId,
int seatsQuantity = 1)
{
return $@"{{
""lookupKey"": ""{lookupKey}"",
""name"": ""{name}"",
""tier"": ""{tier}"",
""features"": [],
""seats"": {{
""type"": ""packaged"",
""quantity"": {seatsQuantity},
""price"": {seatsPrice},
""stripePriceId"": ""{seatsStripePriceId}""
}},
""canUpgradeTo"": [],
""additionalData"": {{
""nameLocalizationKey"": ""{lookupKey}Name"",
""descriptionLocalizationKey"": ""{lookupKey}Description""
}}
}}";
}
}

View File

@@ -22,7 +22,7 @@ public class SecretsManagerSubscriptionUpdateTests
}
public static TheoryData<Plan> NonSmPlans =>
ToPlanTheory([PlanType.Custom, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019]);
ToPlanTheory([PlanType.Custom, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2025, PlanType.FamiliesAnnually2019]);
public static TheoryData<Plan> SmPlans => ToPlanTheory([
PlanType.EnterpriseAnnually2019,

View File

@@ -1,5 +1,8 @@
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Settings;
using Bit.Core.Test.Platform.Mailer.TestMail;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Platform.Mailer;
@@ -9,7 +12,10 @@ public class HandlebarMailRendererTests
[Fact]
public async Task RenderAsync_ReturnsExpectedHtmlAndTxt()
{
var renderer = new HandlebarMailRenderer();
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
var globalSettings = new GlobalSettings { SelfHosted = false };
var renderer = new HandlebarMailRenderer(logger, globalSettings);
var view = new TestMailView { Name = "John Smith" };
var (html, txt) = await renderer.RenderAsync(view);
@@ -17,4 +23,150 @@ public class HandlebarMailRendererTests
Assert.Equal("Hello <b>John Smith</b>", html.Trim());
Assert.Equal("Hello John Smith", txt.Trim());
}
[Fact]
public async Task RenderAsync_LoadsFromDisk_WhenSelfHostedAndFileExists()
{
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var globalSettings = new GlobalSettings
{
SelfHosted = true,
MailTemplateDirectory = tempDir
};
// Create test template files on disk
var htmlTemplatePath = Path.Combine(tempDir, "Bit.Core.Test.Platform.Mailer.TestMail.TestMailView.html.hbs");
var txtTemplatePath = Path.Combine(tempDir, "Bit.Core.Test.Platform.Mailer.TestMail.TestMailView.text.hbs");
await File.WriteAllTextAsync(htmlTemplatePath, "Custom HTML: <b>{{Name}}</b>");
await File.WriteAllTextAsync(txtTemplatePath, "Custom TXT: {{Name}}");
var renderer = new HandlebarMailRenderer(logger, globalSettings);
var view = new TestMailView { Name = "Jane Doe" };
var (html, txt) = await renderer.RenderAsync(view);
Assert.Equal("Custom HTML: <b>Jane Doe</b>", html.Trim());
Assert.Equal("Custom TXT: Jane Doe", txt.Trim());
}
finally
{
// Cleanup
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, true);
}
}
}
[Theory]
[InlineData("../../../etc/passwd")]
[InlineData("../../../../malicious.txt")]
[InlineData("../../malicious.txt")]
[InlineData("../malicious.txt")]
public async Task ReadSourceFromDiskAsync_PrevenetsPathTraversal_WhenMaliciousPathProvided(string maliciousPath)
{
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var globalSettings = new GlobalSettings
{
SelfHosted = true,
MailTemplateDirectory = tempDir
};
// Create a malicious file outside the template directory
var maliciousFile = Path.Combine(Path.GetTempPath(), "malicious.txt");
await File.WriteAllTextAsync(maliciousFile, "Malicious Content");
var renderer = new HandlebarMailRenderer(logger, globalSettings);
// Use reflection to call the private ReadSourceFromDiskAsync method
var method = typeof(HandlebarMailRenderer).GetMethod("ReadSourceFromDiskAsync",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var task = (Task<string?>)method!.Invoke(renderer, new object[] { maliciousPath })!;
var result = await task;
// Should return null and not load the malicious file
Assert.Null(result);
// Verify that a warning was logged for the path traversal attempt
logger.Received(1).Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception, string>>());
// Cleanup malicious file
if (File.Exists(maliciousFile))
{
File.Delete(maliciousFile);
}
}
finally
{
// Cleanup
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, true);
}
}
}
[Fact]
public async Task ReadSourceFromDiskAsync_AllowsValidFileWithDifferentCase_WhenCaseInsensitiveFileSystem()
{
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var globalSettings = new GlobalSettings
{
SelfHosted = true,
MailTemplateDirectory = tempDir
};
// Create a test template file
var templateFileName = "TestTemplate.hbs";
var templatePath = Path.Combine(tempDir, templateFileName);
await File.WriteAllTextAsync(templatePath, "Test Content");
var renderer = new HandlebarMailRenderer(logger, globalSettings);
// Try to read with different case (should work on case-insensitive file systems like Windows/macOS)
var method = typeof(HandlebarMailRenderer).GetMethod("ReadSourceFromDiskAsync",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var task = (Task<string?>)method!.Invoke(renderer, new object[] { templateFileName })!;
var result = await task;
// Should successfully read the file
Assert.Equal("Test Content", result);
// Verify no warning was logged
logger.DidNotReceive().Log(
LogLevel.Warning,
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception, string>>());
}
finally
{
// Cleanup
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, true);
}
}
}
}

View File

@@ -1,18 +1,24 @@
using Bit.Core.Models.Mail;
using Bit.Core.Platform.Mail.Delivery;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Settings;
using Bit.Core.Test.Platform.Mailer.TestMail;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Platform.Mailer;
public class MailerTest
{
[Fact]
public async Task SendEmailAsync()
{
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
var globalSettings = new GlobalSettings { SelfHosted = false };
var deliveryService = Substitute.For<IMailDeliveryService>();
var mailer = new Core.Platform.Mail.Mailer.Mailer(new HandlebarMailRenderer(), deliveryService);
var mailer = new Core.Platform.Mail.Mailer.Mailer(new HandlebarMailRenderer(logger, globalSettings), deliveryService);
var mail = new TestMail.TestMail()
{

View File

@@ -13,7 +13,7 @@ public class StaticStoreTests
var plans = StaticStore.Plans.ToList();
Assert.NotNull(plans);
Assert.NotEmpty(plans);
Assert.Equal(22, plans.Count);
Assert.Equal(23, plans.Count);
}
[Theory]
@@ -34,8 +34,8 @@ public class StaticStoreTests
{
// Ref: https://daniel.haxx.se/blog/2025/05/16/detecting-malicious-unicode/
// URLs can contain unicode characters that to a computer would point to completely seperate domains but to the
// naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a
// URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a
// naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a
// URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a
// url update that could be missed in code review and then if they got a user to that URL Bitwarden could
// consider it equivalent with a cipher in the users vault and offer autofill when we should not.
// GitHub does now show a warning on non-ascii characters but it could still be missed.

View File

@@ -1484,6 +1484,8 @@ public class OrganizationUserRepositoryTests
var organization = await organizationRepository.CreateTestOrganizationAsync();
var user = await userRepository.CreateTestUserAsync();
var orgUser = await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(organization, user);
const string key = "test-key";
orgUser.Key = key;
// Act
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
@@ -1493,6 +1495,7 @@ public class OrganizationUserRepositoryTests
var updatedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
Assert.NotNull(updatedUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, updatedUser.Status);
Assert.Equal(key, updatedUser.Key);
// Annul
await organizationRepository.DeleteAsync(organization);