1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-19 10:28:09 -05:00

[PM-17562] Fix flickering unit test - WebhookIntegrationHandlerTests (#5973)

* [PM-17562] Fix flickering unit test - WebhookIntegrationHandlerTests

* Adjust to using TimeProvider and exact time matches

* Refactored RabittMqIntegrationListenerService and Tests to align on TimeProvider. Cleaned up tests that do not need to use DateTime.UtcNow
This commit is contained in:
Brant DeBow 2025-06-18 10:09:47 -04:00 committed by GitHub
parent 6800bc57f3
commit 502ab4b645
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 37 additions and 22 deletions

View File

@ -20,6 +20,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService
private readonly Lazy<Task<IChannel>> _lazyChannel; private readonly Lazy<Task<IChannel>> _lazyChannel;
private readonly IRabbitMqService _rabbitMqService; private readonly IRabbitMqService _rabbitMqService;
private readonly ILogger<RabbitMqIntegrationListenerService> _logger; private readonly ILogger<RabbitMqIntegrationListenerService> _logger;
private readonly TimeProvider _timeProvider;
public RabbitMqIntegrationListenerService(IIntegrationHandler handler, public RabbitMqIntegrationListenerService(IIntegrationHandler handler,
string routingKey, string routingKey,
@ -27,7 +28,8 @@ public class RabbitMqIntegrationListenerService : BackgroundService
string retryQueueName, string retryQueueName,
int maxRetries, int maxRetries,
IRabbitMqService rabbitMqService, IRabbitMqService rabbitMqService,
ILogger<RabbitMqIntegrationListenerService> logger) ILogger<RabbitMqIntegrationListenerService> logger,
TimeProvider timeProvider)
{ {
_handler = handler; _handler = handler;
_routingKey = routingKey; _routingKey = routingKey;
@ -35,6 +37,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService
_queueName = queueName; _queueName = queueName;
_rabbitMqService = rabbitMqService; _rabbitMqService = rabbitMqService;
_logger = logger; _logger = logger;
_timeProvider = timeProvider;
_maxRetries = maxRetries; _maxRetries = maxRetries;
_lazyChannel = new Lazy<Task<IChannel>>(() => _rabbitMqService.CreateChannelAsync()); _lazyChannel = new Lazy<Task<IChannel>>(() => _rabbitMqService.CreateChannelAsync());
} }
@ -74,7 +77,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService
var integrationMessage = JsonSerializer.Deserialize<IntegrationMessage>(json); var integrationMessage = JsonSerializer.Deserialize<IntegrationMessage>(json);
if (integrationMessage is not null && if (integrationMessage is not null &&
integrationMessage.DelayUntilDate.HasValue && integrationMessage.DelayUntilDate.HasValue &&
integrationMessage.DelayUntilDate.Value > DateTime.UtcNow) integrationMessage.DelayUntilDate.Value > _timeProvider.GetUtcNow().UtcDateTime)
{ {
await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea); await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea);
await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);

View File

@ -9,7 +9,9 @@ using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
namespace Bit.Core.Services; namespace Bit.Core.Services;
public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) public class WebhookIntegrationHandler(
IHttpClientFactory httpClientFactory,
TimeProvider timeProvider)
: IntegrationHandlerBase<WebhookIntegrationConfigurationDetails> : IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
{ {
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
@ -39,7 +41,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory)
if (int.TryParse(value, out var seconds)) if (int.TryParse(value, out var seconds))
{ {
// Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds. // Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds.
result.DelayUntilDate = DateTime.UtcNow.AddSeconds(seconds); result.DelayUntilDate = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime;
} }
else if (DateTimeOffset.TryParseExact(value, else if (DateTimeOffset.TryParseExact(value,
"r", // "r" is the round-trip format: RFC1123 "r", // "r" is the round-trip format: RFC1123

View File

@ -717,7 +717,8 @@ public static class ServiceCollectionExtensions
retryQueueName: integrationRetryQueueName, retryQueueName: integrationRetryQueueName,
maxRetries: maxRetries, maxRetries: maxRetries,
rabbitMqService: provider.GetRequiredService<IRabbitMqService>(), rabbitMqService: provider.GetRequiredService<IRabbitMqService>(),
logger: provider.GetRequiredService<ILogger<RabbitMqIntegrationListenerService>>())); logger: provider.GetRequiredService<ILogger<RabbitMqIntegrationListenerService>>(),
timeProvider: provider.GetRequiredService<TimeProvider>()));
return services; return services;
} }

View File

@ -52,7 +52,6 @@ public class AzureServiceBusIntegrationListenerServiceTests
public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message) public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
{ {
var sutProvider = GetSutProvider(); var sutProvider = GetSutProvider();
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
message.RetryCount = 0; message.RetryCount = 0;
var result = new IntegrationHandlerResult(false, message); var result = new IntegrationHandlerResult(false, message);
@ -71,7 +70,6 @@ public class AzureServiceBusIntegrationListenerServiceTests
public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message) public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
{ {
var sutProvider = GetSutProvider(); var sutProvider = GetSutProvider();
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
message.RetryCount = _maxRetries; message.RetryCount = _maxRetries;
var result = new IntegrationHandlerResult(false, message); var result = new IntegrationHandlerResult(false, message);
result.Retryable = true; result.Retryable = true;
@ -90,12 +88,10 @@ public class AzureServiceBusIntegrationListenerServiceTests
public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message) public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
{ {
var sutProvider = GetSutProvider(); var sutProvider = GetSutProvider();
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
message.RetryCount = 0; message.RetryCount = 0;
var result = new IntegrationHandlerResult(false, message); var result = new IntegrationHandlerResult(false, message);
result.Retryable = true; result.Retryable = true;
result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1);
_handler.HandleAsync(Arg.Any<string>()).Returns(result); _handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = (IntegrationMessage<WebhookIntegrationConfiguration>)IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson())!; var expected = (IntegrationMessage<WebhookIntegrationConfiguration>)IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson())!;
@ -110,7 +106,6 @@ public class AzureServiceBusIntegrationListenerServiceTests
public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message) public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)
{ {
var sutProvider = GetSutProvider(); var sutProvider = GetSutProvider();
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
var result = new IntegrationHandlerResult(true, message); var result = new IntegrationHandlerResult(true, message);
_handler.HandleAsync(Arg.Any<string>()).Returns(result); _handler.HandleAsync(Arg.Any<string>()).Returns(result);

View File

@ -4,6 +4,7 @@ using Bit.Core.Services;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers; using Bit.Test.Common.Helpers;
using Microsoft.Extensions.Time.Testing;
using NSubstitute; using NSubstitute;
using RabbitMQ.Client; using RabbitMQ.Client;
using RabbitMQ.Client.Events; using RabbitMQ.Client.Events;
@ -18,19 +19,24 @@ public class RabbitMqIntegrationListenerServiceTests
private const string _queueName = "test_queue"; private const string _queueName = "test_queue";
private const string _retryQueueName = "test_queue_retry"; private const string _retryQueueName = "test_queue_retry";
private const string _routingKey = "test_routing_key"; private const string _routingKey = "test_routing_key";
private readonly DateTime _now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>(); private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>();
private readonly IRabbitMqService _rabbitMqService = Substitute.For<IRabbitMqService>(); private readonly IRabbitMqService _rabbitMqService = Substitute.For<IRabbitMqService>();
private SutProvider<RabbitMqIntegrationListenerService> GetSutProvider() private SutProvider<RabbitMqIntegrationListenerService> GetSutProvider()
{ {
return new SutProvider<RabbitMqIntegrationListenerService>() var sutProvider = new SutProvider<RabbitMqIntegrationListenerService>()
.SetDependency(_handler) .SetDependency(_handler)
.SetDependency(_rabbitMqService) .SetDependency(_rabbitMqService)
.SetDependency(_queueName, "queueName") .SetDependency(_queueName, "queueName")
.SetDependency(_retryQueueName, "retryQueueName") .SetDependency(_retryQueueName, "retryQueueName")
.SetDependency(_routingKey, "routingKey") .SetDependency(_routingKey, "routingKey")
.SetDependency(_maxRetries, "maxRetries") .SetDependency(_maxRetries, "maxRetries")
.WithFakeTimeProvider()
.Create(); .Create();
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(_now);
return sutProvider;
} }
[Fact] [Fact]
@ -55,7 +61,7 @@ public class RabbitMqIntegrationListenerServiceTests
var cancellationToken = CancellationToken.None; var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken); await sutProvider.Sut.StartAsync(cancellationToken);
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); message.DelayUntilDate = null;
message.RetryCount = 0; message.RetryCount = 0;
var eventArgs = new BasicDeliverEventArgs( var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty, consumerTag: string.Empty,
@ -94,7 +100,7 @@ public class RabbitMqIntegrationListenerServiceTests
var cancellationToken = CancellationToken.None; var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken); await sutProvider.Sut.StartAsync(cancellationToken);
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); message.DelayUntilDate = null;
message.RetryCount = _maxRetries; message.RetryCount = _maxRetries;
var eventArgs = new BasicDeliverEventArgs( var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty, consumerTag: string.Empty,
@ -132,7 +138,7 @@ public class RabbitMqIntegrationListenerServiceTests
var cancellationToken = CancellationToken.None; var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken); await sutProvider.Sut.StartAsync(cancellationToken);
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); message.DelayUntilDate = null;
message.RetryCount = 0; message.RetryCount = 0;
var eventArgs = new BasicDeliverEventArgs( var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty, consumerTag: string.Empty,
@ -145,7 +151,7 @@ public class RabbitMqIntegrationListenerServiceTests
); );
var result = new IntegrationHandlerResult(false, message); var result = new IntegrationHandlerResult(false, message);
result.Retryable = true; result.Retryable = true;
result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); result.DelayUntilDate = _now.AddMinutes(1);
_handler.HandleAsync(Arg.Any<string>()).Returns(result); _handler.HandleAsync(Arg.Any<string>()).Returns(result);
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson()); var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
@ -173,7 +179,7 @@ public class RabbitMqIntegrationListenerServiceTests
var cancellationToken = CancellationToken.None; var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken); await sutProvider.Sut.StartAsync(cancellationToken);
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); message.DelayUntilDate = null;
var eventArgs = new BasicDeliverEventArgs( var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty, consumerTag: string.Empty,
deliveryTag: 0, deliveryTag: 0,
@ -205,7 +211,7 @@ public class RabbitMqIntegrationListenerServiceTests
var cancellationToken = CancellationToken.None; var cancellationToken = CancellationToken.None;
await sutProvider.Sut.StartAsync(cancellationToken); await sutProvider.Sut.StartAsync(cancellationToken);
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); message.DelayUntilDate = _now.AddMinutes(1);
var eventArgs = new BasicDeliverEventArgs( var eventArgs = new BasicDeliverEventArgs(
consumerTag: string.Empty, consumerTag: string.Empty,
deliveryTag: 0, deliveryTag: 0,

View File

@ -5,6 +5,7 @@ using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers; using Bit.Test.Common.Helpers;
using Bit.Test.Common.MockedHttpClient; using Bit.Test.Common.MockedHttpClient;
using Microsoft.Extensions.Time.Testing;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
@ -33,6 +34,7 @@ public class WebhookIntegrationHandlerTests
return new SutProvider<WebhookIntegrationHandler>() return new SutProvider<WebhookIntegrationHandler>()
.SetDependency(clientFactory) .SetDependency(clientFactory)
.WithFakeTimeProvider()
.Create(); .Create();
} }
@ -62,9 +64,13 @@ public class WebhookIntegrationHandlerTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsNotBeforUtc(IntegrationMessage<WebhookIntegrationConfigurationDetails> message) public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{ {
var sutProvider = GetSutProvider(); var sutProvider = GetSutProvider();
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
var retryAfter = now.AddSeconds(60);
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
_handler.Fallback _handler.Fallback
@ -78,19 +84,21 @@ public class WebhookIntegrationHandlerTests
Assert.True(result.Retryable); Assert.True(result.Retryable);
Assert.Equal(result.Message, message); Assert.Equal(result.Message, message);
Assert.True(result.DelayUntilDate.HasValue); Assert.True(result.DelayUntilDate.HasValue);
Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); Assert.Equal(retryAfter, result.DelayUntilDate.Value);
Assert.Equal("Too Many Requests", result.FailureReason); Assert.Equal("Too Many Requests", result.FailureReason);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsNotBeforUtc(IntegrationMessage<WebhookIntegrationConfigurationDetails> message) public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{ {
var sutProvider = GetSutProvider(); var sutProvider = GetSutProvider();
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
var retryAfter = now.AddSeconds(60);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
_handler.Fallback _handler.Fallback
.WithStatusCode(HttpStatusCode.TooManyRequests) .WithStatusCode(HttpStatusCode.TooManyRequests)
.WithHeader("Retry-After", DateTime.UtcNow.AddSeconds(60).ToString("r")) // "r" is the round-trip format: RFC1123 .WithHeader("Retry-After", retryAfter.ToString("r"))
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>")); .WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
var result = await sutProvider.Sut.HandleAsync(message); var result = await sutProvider.Sut.HandleAsync(message);
@ -99,7 +107,7 @@ public class WebhookIntegrationHandlerTests
Assert.True(result.Retryable); Assert.True(result.Retryable);
Assert.Equal(result.Message, message); Assert.Equal(result.Message, message);
Assert.True(result.DelayUntilDate.HasValue); Assert.True(result.DelayUntilDate.HasValue);
Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); Assert.Equal(retryAfter, result.DelayUntilDate.Value);
Assert.Equal("Too Many Requests", result.FailureReason); Assert.Equal("Too Many Requests", result.FailureReason);
} }