up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Notifier.Worker.Escalation;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Tests.Escalation;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for inbox channel adapters.
|
||||
/// </summary>
|
||||
public sealed class InboxChannelTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly InAppInboxChannel _inboxChannel;
|
||||
private readonly CliNotificationChannel _cliChannel;
|
||||
|
||||
public InboxChannelTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 15, 10, 0, 0, TimeSpan.Zero));
|
||||
|
||||
_inboxChannel = new InAppInboxChannel(
|
||||
null,
|
||||
_timeProvider,
|
||||
NullLogger<InAppInboxChannel>.Instance);
|
||||
|
||||
_cliChannel = new CliNotificationChannel(
|
||||
_timeProvider,
|
||||
NullLogger<CliNotificationChannel>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_SendAsync_CreatesNotification()
|
||||
{
|
||||
var notification = CreateTestNotification("tenant-1", "user-1", "notif-1");
|
||||
|
||||
var result = await _inboxChannel.SendAsync(notification);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.NotificationId.Should().Be("notif-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_ListAsync_ReturnsNotifications()
|
||||
{
|
||||
var notification = CreateTestNotification("tenant-1", "user-1", "notif-1");
|
||||
await _inboxChannel.SendAsync(notification);
|
||||
|
||||
var result = await _inboxChannel.ListAsync("tenant-1", "user-1");
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
result[0].NotificationId.Should().Be("notif-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_ListAsync_FiltersUnread()
|
||||
{
|
||||
var notif1 = CreateTestNotification("tenant-1", "user-1", "notif-1");
|
||||
var notif2 = CreateTestNotification("tenant-1", "user-1", "notif-2");
|
||||
|
||||
await _inboxChannel.SendAsync(notif1);
|
||||
await _inboxChannel.SendAsync(notif2);
|
||||
await _inboxChannel.MarkReadAsync("tenant-1", "user-1", "notif-1");
|
||||
|
||||
var result = await _inboxChannel.ListAsync("tenant-1", "user-1", new InboxQuery { IsRead = false });
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
result[0].NotificationId.Should().Be("notif-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_ListAsync_FiltersByType()
|
||||
{
|
||||
var incident = CreateTestNotification("tenant-1", "user-1", "notif-1") with
|
||||
{
|
||||
Type = InboxNotificationType.Incident
|
||||
};
|
||||
var system = CreateTestNotification("tenant-1", "user-1", "notif-2") with
|
||||
{
|
||||
Type = InboxNotificationType.System
|
||||
};
|
||||
|
||||
await _inboxChannel.SendAsync(incident);
|
||||
await _inboxChannel.SendAsync(system);
|
||||
|
||||
var result = await _inboxChannel.ListAsync("tenant-1", "user-1",
|
||||
new InboxQuery { Type = InboxNotificationType.Incident });
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
result[0].NotificationId.Should().Be("notif-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_ListAsync_FiltersByMinPriority()
|
||||
{
|
||||
var low = CreateTestNotification("tenant-1", "user-1", "notif-1") with { Priority = InboxPriority.Low };
|
||||
var high = CreateTestNotification("tenant-1", "user-1", "notif-2") with { Priority = InboxPriority.High };
|
||||
var urgent = CreateTestNotification("tenant-1", "user-1", "notif-3") with { Priority = InboxPriority.Urgent };
|
||||
|
||||
await _inboxChannel.SendAsync(low);
|
||||
await _inboxChannel.SendAsync(high);
|
||||
await _inboxChannel.SendAsync(urgent);
|
||||
|
||||
var result = await _inboxChannel.ListAsync("tenant-1", "user-1",
|
||||
new InboxQuery { MinPriority = InboxPriority.High });
|
||||
|
||||
result.Should().HaveCount(2);
|
||||
result.Should().OnlyContain(n => n.Priority >= InboxPriority.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_ListAsync_ExcludesExpired()
|
||||
{
|
||||
var active = CreateTestNotification("tenant-1", "user-1", "notif-1") with
|
||||
{
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddHours(1)
|
||||
};
|
||||
var expired = CreateTestNotification("tenant-1", "user-1", "notif-2") with
|
||||
{
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddHours(-1)
|
||||
};
|
||||
|
||||
await _inboxChannel.SendAsync(active);
|
||||
await _inboxChannel.SendAsync(expired);
|
||||
|
||||
var result = await _inboxChannel.ListAsync("tenant-1", "user-1");
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
result[0].NotificationId.Should().Be("notif-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_ListAsync_IncludesExpiredWhenRequested()
|
||||
{
|
||||
var active = CreateTestNotification("tenant-1", "user-1", "notif-1") with
|
||||
{
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddHours(1)
|
||||
};
|
||||
var expired = CreateTestNotification("tenant-1", "user-1", "notif-2") with
|
||||
{
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddHours(-1)
|
||||
};
|
||||
|
||||
await _inboxChannel.SendAsync(active);
|
||||
await _inboxChannel.SendAsync(expired);
|
||||
|
||||
var result = await _inboxChannel.ListAsync("tenant-1", "user-1",
|
||||
new InboxQuery { IncludeExpired = true });
|
||||
|
||||
result.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_ListAsync_RespectsLimit()
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
await _inboxChannel.SendAsync(CreateTestNotification("tenant-1", "user-1", $"notif-{i}"));
|
||||
}
|
||||
|
||||
var result = await _inboxChannel.ListAsync("tenant-1", "user-1",
|
||||
new InboxQuery { Limit = 5 });
|
||||
|
||||
result.Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_MarkReadAsync_MarksNotificationAsRead()
|
||||
{
|
||||
var notification = CreateTestNotification("tenant-1", "user-1", "notif-1");
|
||||
await _inboxChannel.SendAsync(notification);
|
||||
|
||||
var result = await _inboxChannel.MarkReadAsync("tenant-1", "user-1", "notif-1");
|
||||
|
||||
result.Should().BeTrue();
|
||||
|
||||
var list = await _inboxChannel.ListAsync("tenant-1", "user-1");
|
||||
list[0].IsRead.Should().BeTrue();
|
||||
list[0].ReadAt.Should().Be(_timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_MarkReadAsync_ReturnsFalseForNonexistent()
|
||||
{
|
||||
var result = await _inboxChannel.MarkReadAsync("tenant-1", "user-1", "nonexistent");
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_MarkAllReadAsync_MarksAllAsRead()
|
||||
{
|
||||
await _inboxChannel.SendAsync(CreateTestNotification("tenant-1", "user-1", "notif-1"));
|
||||
await _inboxChannel.SendAsync(CreateTestNotification("tenant-1", "user-1", "notif-2"));
|
||||
await _inboxChannel.SendAsync(CreateTestNotification("tenant-1", "user-1", "notif-3"));
|
||||
|
||||
var result = await _inboxChannel.MarkAllReadAsync("tenant-1", "user-1");
|
||||
|
||||
result.Should().Be(3);
|
||||
|
||||
var unread = await _inboxChannel.GetUnreadCountAsync("tenant-1", "user-1");
|
||||
unread.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_GetUnreadCountAsync_ReturnsCorrectCount()
|
||||
{
|
||||
await _inboxChannel.SendAsync(CreateTestNotification("tenant-1", "user-1", "notif-1"));
|
||||
await _inboxChannel.SendAsync(CreateTestNotification("tenant-1", "user-1", "notif-2"));
|
||||
await _inboxChannel.SendAsync(CreateTestNotification("tenant-1", "user-1", "notif-3"));
|
||||
await _inboxChannel.MarkReadAsync("tenant-1", "user-1", "notif-1");
|
||||
|
||||
var result = await _inboxChannel.GetUnreadCountAsync("tenant-1", "user-1");
|
||||
|
||||
result.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_GetUnreadCountAsync_ExcludesExpired()
|
||||
{
|
||||
var active = CreateTestNotification("tenant-1", "user-1", "notif-1") with
|
||||
{
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddHours(1)
|
||||
};
|
||||
var expired = CreateTestNotification("tenant-1", "user-1", "notif-2") with
|
||||
{
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddHours(-1)
|
||||
};
|
||||
|
||||
await _inboxChannel.SendAsync(active);
|
||||
await _inboxChannel.SendAsync(expired);
|
||||
|
||||
var result = await _inboxChannel.GetUnreadCountAsync("tenant-1", "user-1");
|
||||
|
||||
result.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_IsolatesByTenantAndUser()
|
||||
{
|
||||
await _inboxChannel.SendAsync(CreateTestNotification("tenant-1", "user-1", "notif-1"));
|
||||
await _inboxChannel.SendAsync(CreateTestNotification("tenant-1", "user-2", "notif-2"));
|
||||
await _inboxChannel.SendAsync(CreateTestNotification("tenant-2", "user-1", "notif-3"));
|
||||
|
||||
var tenant1User1 = await _inboxChannel.ListAsync("tenant-1", "user-1");
|
||||
var tenant1User2 = await _inboxChannel.ListAsync("tenant-1", "user-2");
|
||||
var tenant2User1 = await _inboxChannel.ListAsync("tenant-2", "user-1");
|
||||
|
||||
tenant1User1.Should().HaveCount(1).And.Contain(n => n.NotificationId == "notif-1");
|
||||
tenant1User2.Should().HaveCount(1).And.Contain(n => n.NotificationId == "notif-2");
|
||||
tenant2User1.Should().HaveCount(1).And.Contain(n => n.NotificationId == "notif-3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InApp_ListAsync_SortsByPriorityAndCreatedAt()
|
||||
{
|
||||
var low = CreateTestNotification("tenant-1", "user-1", "notif-low") with { Priority = InboxPriority.Low };
|
||||
await _inboxChannel.SendAsync(low);
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
|
||||
var high = CreateTestNotification("tenant-1", "user-1", "notif-high") with { Priority = InboxPriority.High };
|
||||
await _inboxChannel.SendAsync(high);
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
|
||||
var urgent = CreateTestNotification("tenant-1", "user-1", "notif-urgent") with { Priority = InboxPriority.Urgent };
|
||||
await _inboxChannel.SendAsync(urgent);
|
||||
|
||||
var result = await _inboxChannel.ListAsync("tenant-1", "user-1");
|
||||
|
||||
result[0].NotificationId.Should().Be("notif-urgent");
|
||||
result[1].NotificationId.Should().Be("notif-high");
|
||||
result[2].NotificationId.Should().Be("notif-low");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cli_SendAsync_CreatesNotification()
|
||||
{
|
||||
var notification = CreateTestNotification("tenant-1", "user-1", "notif-1");
|
||||
|
||||
var result = await _cliChannel.SendAsync(notification);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.NotificationId.Should().Be("notif-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cli_ListAsync_ReturnsNotifications()
|
||||
{
|
||||
var notification = CreateTestNotification("tenant-1", "user-1", "notif-1");
|
||||
await _cliChannel.SendAsync(notification);
|
||||
|
||||
var result = await _cliChannel.ListAsync("tenant-1", "user-1");
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cli_FormatForCli_FormatsCorrectly()
|
||||
{
|
||||
var notification = new InboxNotification
|
||||
{
|
||||
NotificationId = "notif-1",
|
||||
TenantId = "tenant-1",
|
||||
UserId = "user-1",
|
||||
Type = InboxNotificationType.Incident,
|
||||
Title = "Critical Alert",
|
||||
Body = "Server down",
|
||||
Priority = InboxPriority.Urgent,
|
||||
IsRead = false,
|
||||
CreatedAt = new DateTimeOffset(2025, 1, 15, 10, 0, 0, TimeSpan.Zero)
|
||||
};
|
||||
|
||||
var formatted = CliNotificationChannel.FormatForCli(notification);
|
||||
|
||||
formatted.Should().Contain("[!!!]");
|
||||
formatted.Should().Contain("●");
|
||||
formatted.Should().Contain("Critical Alert");
|
||||
formatted.Should().Contain("Server down");
|
||||
formatted.Should().Contain("2025-01-15");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cli_FormatForCli_ShowsReadMarker()
|
||||
{
|
||||
var notification = new InboxNotification
|
||||
{
|
||||
NotificationId = "notif-1",
|
||||
TenantId = "tenant-1",
|
||||
UserId = "user-1",
|
||||
Type = InboxNotificationType.Incident,
|
||||
Title = "Alert",
|
||||
Body = "Details",
|
||||
Priority = InboxPriority.Normal,
|
||||
IsRead = true,
|
||||
CreatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
var formatted = CliNotificationChannel.FormatForCli(notification);
|
||||
|
||||
formatted.Should().NotContain("●");
|
||||
formatted.Should().Contain("[*]");
|
||||
}
|
||||
|
||||
private static InboxNotification CreateTestNotification(string tenantId, string userId, string notificationId) => new()
|
||||
{
|
||||
NotificationId = notificationId,
|
||||
TenantId = tenantId,
|
||||
UserId = userId,
|
||||
Type = InboxNotificationType.Incident,
|
||||
Title = "Test Alert",
|
||||
Body = "This is a test notification",
|
||||
Priority = InboxPriority.Normal
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user