run test on phase 24 and 33
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
[Collection("Integration")]
|
||||
public class DlqRoutingTests
|
||||
{
|
||||
private readonly ApiFixture _fixture;
|
||||
|
||||
public DlqRoutingTests(ApiFixture fixture) => _fixture = fixture;
|
||||
|
||||
[Fact]
|
||||
public async Task PagingDlq_TtlDeadLettersToEscalationQueue()
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var opts = scope.ServiceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptions<RabbitMqOptions>>();
|
||||
|
||||
var routed = await DlqRoutingProbe.DlqTtlRoutesToEscalationQueueAsync(opts);
|
||||
routed.Should().BeTrue("DLQ x-message-ttl should dead-letter to alerts.escalation.queue");
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ public class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
["RabbitMq:Username"] = "guest",
|
||||
["RabbitMq:Password"] = "guest",
|
||||
["RabbitMq:PagingAckTimeoutMs"] = "5000",
|
||||
["RabbitMq:VirtualHost"] = "vigilcare_test",
|
||||
["Kafka:NotificationPublisherGroupId"] = "notification-publisher-integration-test",
|
||||
["Kafka:NotificationPublisherAutoOffsetReset"] = "Latest",
|
||||
["Fhir:ApiKey"] = "dev-integration-key-change-in-production",
|
||||
["ApiKey:Gateway"] = GatewayAuthHelper.DevGatewayKey,
|
||||
});
|
||||
@@ -59,6 +62,15 @@ public class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
await DbResetHelper.ResetAsync(migrateDb);
|
||||
}
|
||||
|
||||
await RabbitMqTestHelper.EnsureVirtualHostAsync(new RabbitMqOptions
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 5674,
|
||||
Username = "guest",
|
||||
Password = "guest",
|
||||
VirtualHost = "vigilcare_test",
|
||||
});
|
||||
|
||||
using var scope = Services.CreateScope();
|
||||
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
|
||||
var server = redis.GetServer(redis.GetEndPoints().First());
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RabbitMQ.Client;
|
||||
|
||||
public static class DlqRoutingProbe
|
||||
{
|
||||
public static async Task<bool> DlqTtlRoutesToEscalationQueueAsync(IOptions<RabbitMqOptions> opts)
|
||||
{
|
||||
var o = opts.Value;
|
||||
var factory = RabbitMqConnectionFactory.Create(o);
|
||||
|
||||
using var connection = factory.CreateConnection("dlq-routing-probe");
|
||||
using var channel = connection.CreateModel();
|
||||
|
||||
RabbitMqTestHelper.PurgeNotificationQueues(opts);
|
||||
|
||||
var body = Encoding.UTF8.GetBytes(
|
||||
"""{"alertId":"00000000-0000-0000-0000-000000000099","encounterId":"00000000-0000-0000-0000-000000000088"}""");
|
||||
|
||||
channel.BasicPublish(
|
||||
exchange: "",
|
||||
routingKey: "alerts.paging.dlq",
|
||||
basicProperties: null,
|
||||
body: body);
|
||||
|
||||
var dlqHasMessage = false;
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
await Task.Delay(250);
|
||||
var dlq = channel.QueueDeclarePassive("alerts.paging.dlq");
|
||||
if (dlq.MessageCount > 0)
|
||||
{
|
||||
dlqHasMessage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dlqHasMessage.Should().BeTrue("probe message should land in alerts.paging.dlq");
|
||||
|
||||
var deadline = DateTimeOffset.UtcNow.AddSeconds(15);
|
||||
while (DateTimeOffset.UtcNow < deadline)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
var dlq = channel.QueueDeclarePassive("alerts.paging.dlq");
|
||||
if (dlq.MessageCount == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,56 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Exceptions;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
public static class RabbitMqTestHelper
|
||||
{
|
||||
private static readonly string[] NotificationQueues =
|
||||
[
|
||||
"alerts.paging.queue",
|
||||
"alerts.paging.dlq",
|
||||
"alerts.escalation.queue",
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Creates the configured virtual host and grants the test user full access.
|
||||
/// Integration tests use a dedicated vhost so a concurrently running dev API
|
||||
/// on the default "/" vhost cannot steal paging messages or recreate the DLQ TTL.
|
||||
/// </summary>
|
||||
public static async Task EnsureVirtualHostAsync(RabbitMqOptions o)
|
||||
{
|
||||
if (string.IsNullOrEmpty(o.VirtualHost) || o.VirtualHost == "/")
|
||||
return;
|
||||
|
||||
using var http = new HttpClient();
|
||||
var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{o.Username}:{o.Password}"));
|
||||
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
|
||||
|
||||
var mgmtBase = $"http://{o.Host}:{o.Port + 10000}";
|
||||
var vhost = Uri.EscapeDataString(o.VirtualHost);
|
||||
|
||||
using var create = await http.PutAsync($"{mgmtBase}/api/vhosts/{vhost}", null);
|
||||
create.EnsureSuccessStatusCode();
|
||||
|
||||
var permissions = JsonSerializer.Serialize(new
|
||||
{
|
||||
configure = ".*",
|
||||
write = ".*",
|
||||
read = ".*",
|
||||
});
|
||||
using var grant = await http.PutAsync(
|
||||
$"{mgmtBase}/api/permissions/{vhost}/{Uri.EscapeDataString(o.Username)}",
|
||||
new StringContent(permissions, Encoding.UTF8, "application/json"));
|
||||
grant.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public static void PurgeNotificationQueues(IOptions<RabbitMqOptions> opts)
|
||||
{
|
||||
var o = opts.Value;
|
||||
var factory = new ConnectionFactory
|
||||
{
|
||||
HostName = o.Host,
|
||||
Port = o.Port,
|
||||
UserName = o.Username,
|
||||
Password = o.Password,
|
||||
};
|
||||
|
||||
using var connection = factory.CreateConnection("test-queue-purge");
|
||||
using var connection = CreateConnection(o, "test-queue-purge");
|
||||
using var channel = connection.CreateModel();
|
||||
|
||||
// x-message-ttl is immutable once the queue exists. A concurrently running dev API
|
||||
@@ -36,6 +71,67 @@ public static class RabbitMqTestHelper
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for stale alert.generated Kafka messages to finish the paging → DLQ → escalation
|
||||
/// cycle. Purging alone is not enough: NotificationPublisher keeps republishing backlog
|
||||
/// after the purge, and prefetch=1 leaves one in-flight page ahead of the test alert.
|
||||
/// </summary>
|
||||
public static async Task WaitForNotificationPipelineIdleAsync(
|
||||
IOptions<RabbitMqOptions> opts,
|
||||
TimeSpan maxWait)
|
||||
{
|
||||
var o = opts.Value;
|
||||
// One full cycle: paging timeout + DLQ TTL + worker margin.
|
||||
var settle = TimeSpan.FromMilliseconds(o.PagingAckTimeoutMs * 2 + 10_000);
|
||||
var deadline = DateTimeOffset.UtcNow.Add(maxWait);
|
||||
DateTimeOffset? lastBusy = null;
|
||||
|
||||
while (DateTimeOffset.UtcNow < deadline)
|
||||
{
|
||||
if (!AreReadyQueuesEmpty(o))
|
||||
{
|
||||
lastBusy = DateTimeOffset.UtcNow;
|
||||
await Task.Delay(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
lastBusy ??= DateTimeOffset.UtcNow;
|
||||
|
||||
if (DateTimeOffset.UtcNow - lastBusy.Value >= settle)
|
||||
return;
|
||||
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AreReadyQueuesEmpty(RabbitMqOptions o)
|
||||
{
|
||||
using var connection = CreateConnection(o, "test-queue-idle-check");
|
||||
using var channel = connection.CreateModel();
|
||||
|
||||
foreach (var queue in NotificationQueues)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stats = channel.QueueDeclarePassive(queue);
|
||||
if (stats.MessageCount > 0)
|
||||
return false;
|
||||
}
|
||||
catch (OperationInterruptedException)
|
||||
{
|
||||
// Queue may not exist yet on a cold broker.
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ConnectionFactory CreateFactory(RabbitMqOptions o) =>
|
||||
RabbitMqConnectionFactory.Create(o);
|
||||
|
||||
private static IConnection CreateConnection(RabbitMqOptions o, string name) =>
|
||||
CreateFactory(o).CreateConnection(name);
|
||||
|
||||
private static void RecreatePagingDlq(IModel channel, int pagingAckTimeoutMs)
|
||||
{
|
||||
const string dlq = "alerts.paging.dlq";
|
||||
|
||||
@@ -85,13 +85,22 @@ public class NotificationPipelineTests : IAsyncLifetime
|
||||
[Fact]
|
||||
public async Task CriticalAlert_Unacknowledged_EscalatesAfterTimeout()
|
||||
{
|
||||
using (var scope = _fixture.Services.CreateScope())
|
||||
{
|
||||
var rabbitOpts = scope.ServiceProvider.GetRequiredService<IOptions<RabbitMqOptions>>();
|
||||
RabbitMqTestHelper.PurgeNotificationQueues(rabbitOpts);
|
||||
await RabbitMqTestHelper.WaitForNotificationPipelineIdleAsync(
|
||||
rabbitOpts, TimeSpan.FromSeconds(90));
|
||||
RabbitMqTestHelper.PurgeNotificationQueues(rabbitOpts);
|
||||
}
|
||||
|
||||
var encounterId = await CreateActiveEncounterAsync();
|
||||
var alertId = await IngestCriticalPotassiumAsync(encounterId);
|
||||
|
||||
// paging worker (≤6s) → DLQ (5s TTL) → escalation worker.
|
||||
// Poll instead of a fixed sleep: earlier tests may leave paging jobs queued
|
||||
// (prefetch=1), so wall-clock time varies across the full integration suite.
|
||||
var deadline = DateTimeOffset.UtcNow.AddSeconds(60);
|
||||
var perAlertCycle = TimeSpan.FromMilliseconds(
|
||||
_fixture.Services.GetRequiredService<IOptions<RabbitMqOptions>>().Value.PagingAckTimeoutMs * 2 + 12_000);
|
||||
var deadline = DateTimeOffset.UtcNow.Add(perAlertCycle);
|
||||
AlertStatus status;
|
||||
do
|
||||
{
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
[Collection("Integration")]
|
||||
public class RabbitMqOptionsTests
|
||||
{
|
||||
private readonly ApiFixture _fixture;
|
||||
|
||||
public RabbitMqOptionsTests(ApiFixture fixture) => _fixture = fixture;
|
||||
|
||||
[Fact]
|
||||
public void PagingAckTimeoutMs_IsConfiguredForIntegrationTests()
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var opts = scope.ServiceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptions<RabbitMqOptions>>().Value;
|
||||
opts.PagingAckTimeoutMs.Should().Be(5000);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user