up
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
using StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(NotifyPostgresCollection.Name)]
|
||||
public sealed class ChannelRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NotifyPostgresFixture _fixture;
|
||||
private readonly ChannelRepository _repository;
|
||||
private readonly string _tenantId = Guid.NewGuid().ToString();
|
||||
|
||||
public ChannelRepositoryTests(NotifyPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
var options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
var dataSource = new NotifyDataSource(Options.Create(options), NullLogger<NotifyDataSource>.Instance);
|
||||
_repository = new ChannelRepository(dataSource, NullLogger<ChannelRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndGetById_RoundTripsChannel()
|
||||
{
|
||||
// Arrange
|
||||
var channel = new ChannelEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "email-primary",
|
||||
ChannelType = ChannelType.Email,
|
||||
Enabled = true,
|
||||
Config = "{\"smtpHost\": \"smtp.example.com\"}"
|
||||
};
|
||||
|
||||
// Act
|
||||
await _repository.CreateAsync(channel);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, channel.Id);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(channel.Id);
|
||||
fetched.Name.Should().Be("email-primary");
|
||||
fetched.ChannelType.Should().Be(ChannelType.Email);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByName_ReturnsCorrectChannel()
|
||||
{
|
||||
// Arrange
|
||||
var channel = CreateChannel("slack-alerts", ChannelType.Slack);
|
||||
await _repository.CreateAsync(channel);
|
||||
|
||||
// Act
|
||||
var fetched = await _repository.GetByNameAsync(_tenantId, "slack-alerts");
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(channel.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAll_ReturnsAllChannelsForTenant()
|
||||
{
|
||||
// Arrange
|
||||
var channel1 = CreateChannel("channel1", ChannelType.Email);
|
||||
var channel2 = CreateChannel("channel2", ChannelType.Slack);
|
||||
await _repository.CreateAsync(channel1);
|
||||
await _repository.CreateAsync(channel2);
|
||||
|
||||
// Act
|
||||
var channels = await _repository.GetAllAsync(_tenantId);
|
||||
|
||||
// Assert
|
||||
channels.Should().HaveCount(2);
|
||||
channels.Select(c => c.Name).Should().Contain(["channel1", "channel2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAll_FiltersByEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var enabledChannel = CreateChannel("enabled", ChannelType.Email);
|
||||
var disabledChannel = new ChannelEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "disabled",
|
||||
ChannelType = ChannelType.Email,
|
||||
Enabled = false
|
||||
};
|
||||
await _repository.CreateAsync(enabledChannel);
|
||||
await _repository.CreateAsync(disabledChannel);
|
||||
|
||||
// Act
|
||||
var enabledChannels = await _repository.GetAllAsync(_tenantId, enabled: true);
|
||||
|
||||
// Assert
|
||||
enabledChannels.Should().HaveCount(1);
|
||||
enabledChannels[0].Name.Should().Be("enabled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAll_FiltersByChannelType()
|
||||
{
|
||||
// Arrange
|
||||
var emailChannel = CreateChannel("email", ChannelType.Email);
|
||||
var slackChannel = CreateChannel("slack", ChannelType.Slack);
|
||||
await _repository.CreateAsync(emailChannel);
|
||||
await _repository.CreateAsync(slackChannel);
|
||||
|
||||
// Act
|
||||
var slackChannels = await _repository.GetAllAsync(_tenantId, channelType: ChannelType.Slack);
|
||||
|
||||
// Assert
|
||||
slackChannels.Should().HaveCount(1);
|
||||
slackChannels[0].Name.Should().Be("slack");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_ModifiesChannel()
|
||||
{
|
||||
// Arrange
|
||||
var channel = CreateChannel("update-test", ChannelType.Email);
|
||||
await _repository.CreateAsync(channel);
|
||||
|
||||
// Act
|
||||
var updated = new ChannelEntity
|
||||
{
|
||||
Id = channel.Id,
|
||||
TenantId = _tenantId,
|
||||
Name = "update-test",
|
||||
ChannelType = ChannelType.Email,
|
||||
Enabled = false,
|
||||
Config = "{\"updated\": true}"
|
||||
};
|
||||
var result = await _repository.UpdateAsync(updated);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, channel.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.Enabled.Should().BeFalse();
|
||||
fetched.Config.Should().Contain("updated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_RemovesChannel()
|
||||
{
|
||||
// Arrange
|
||||
var channel = CreateChannel("delete-test", ChannelType.Email);
|
||||
await _repository.CreateAsync(channel);
|
||||
|
||||
// Act
|
||||
var result = await _repository.DeleteAsync(_tenantId, channel.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, channel.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetEnabledByType_ReturnsOnlyEnabledChannelsOfType()
|
||||
{
|
||||
// Arrange
|
||||
var enabledEmail = CreateChannel("enabled-email", ChannelType.Email);
|
||||
var disabledEmail = new ChannelEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "disabled-email",
|
||||
ChannelType = ChannelType.Email,
|
||||
Enabled = false
|
||||
};
|
||||
var enabledSlack = CreateChannel("enabled-slack", ChannelType.Slack);
|
||||
await _repository.CreateAsync(enabledEmail);
|
||||
await _repository.CreateAsync(disabledEmail);
|
||||
await _repository.CreateAsync(enabledSlack);
|
||||
|
||||
// Act
|
||||
var channels = await _repository.GetEnabledByTypeAsync(_tenantId, ChannelType.Email);
|
||||
|
||||
// Assert
|
||||
channels.Should().HaveCount(1);
|
||||
channels[0].Name.Should().Be("enabled-email");
|
||||
}
|
||||
|
||||
private ChannelEntity CreateChannel(string name, ChannelType type) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = name,
|
||||
ChannelType = type,
|
||||
Enabled = true
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
using StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(NotifyPostgresCollection.Name)]
|
||||
public sealed class DeliveryRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NotifyPostgresFixture _fixture;
|
||||
private readonly DeliveryRepository _repository;
|
||||
private readonly string _tenantId = Guid.NewGuid().ToString();
|
||||
|
||||
public DeliveryRepositoryTests(NotifyPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
var options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
var dataSource = new NotifyDataSource(Options.Create(options), NullLogger<NotifyDataSource>.Instance);
|
||||
_repository = new DeliveryRepository(dataSource, NullLogger<DeliveryRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndGetById_RoundTripsDelivery()
|
||||
{
|
||||
// Arrange
|
||||
var delivery = CreateDelivery();
|
||||
|
||||
// Act
|
||||
await _repository.CreateAsync(delivery);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, delivery.Id);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(delivery.Id);
|
||||
fetched.Recipient.Should().Be("user@example.com");
|
||||
fetched.Status.Should().Be(DeliveryStatus.Pending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPending_ReturnsPendingDeliveries()
|
||||
{
|
||||
// Arrange
|
||||
var pending = CreateDelivery();
|
||||
await _repository.CreateAsync(pending);
|
||||
|
||||
// Act
|
||||
var pendingDeliveries = await _repository.GetPendingAsync(_tenantId);
|
||||
|
||||
// Assert
|
||||
pendingDeliveries.Should().HaveCount(1);
|
||||
pendingDeliveries[0].Id.Should().Be(pending.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByStatus_ReturnsDeliveriesWithStatus()
|
||||
{
|
||||
// Arrange
|
||||
var delivery = CreateDelivery();
|
||||
await _repository.CreateAsync(delivery);
|
||||
|
||||
// Act
|
||||
var deliveries = await _repository.GetByStatusAsync(_tenantId, DeliveryStatus.Pending);
|
||||
|
||||
// Assert
|
||||
deliveries.Should().HaveCount(1);
|
||||
deliveries[0].Status.Should().Be(DeliveryStatus.Pending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByCorrelationId_ReturnsCorrelatedDeliveries()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var delivery = new DeliveryEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
ChannelId = Guid.NewGuid(),
|
||||
Recipient = "user@example.com",
|
||||
EventType = "scan.completed",
|
||||
CorrelationId = correlationId
|
||||
};
|
||||
await _repository.CreateAsync(delivery);
|
||||
|
||||
// Act
|
||||
var deliveries = await _repository.GetByCorrelationIdAsync(_tenantId, correlationId);
|
||||
|
||||
// Assert
|
||||
deliveries.Should().HaveCount(1);
|
||||
deliveries[0].CorrelationId.Should().Be(correlationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkQueued_UpdatesStatus()
|
||||
{
|
||||
// Arrange
|
||||
var delivery = CreateDelivery();
|
||||
await _repository.CreateAsync(delivery);
|
||||
|
||||
// Act
|
||||
var result = await _repository.MarkQueuedAsync(_tenantId, delivery.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, delivery.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.Status.Should().Be(DeliveryStatus.Queued);
|
||||
fetched.QueuedAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkSent_UpdatesStatusAndExternalId()
|
||||
{
|
||||
// Arrange
|
||||
var delivery = CreateDelivery();
|
||||
await _repository.CreateAsync(delivery);
|
||||
await _repository.MarkQueuedAsync(_tenantId, delivery.Id);
|
||||
|
||||
// Act
|
||||
var result = await _repository.MarkSentAsync(_tenantId, delivery.Id, "external-123");
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, delivery.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.Status.Should().Be(DeliveryStatus.Sent);
|
||||
fetched.ExternalId.Should().Be("external-123");
|
||||
fetched.SentAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkDelivered_UpdatesStatus()
|
||||
{
|
||||
// Arrange
|
||||
var delivery = CreateDelivery();
|
||||
await _repository.CreateAsync(delivery);
|
||||
await _repository.MarkSentAsync(_tenantId, delivery.Id);
|
||||
|
||||
// Act
|
||||
var result = await _repository.MarkDeliveredAsync(_tenantId, delivery.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, delivery.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.Status.Should().Be(DeliveryStatus.Delivered);
|
||||
fetched.DeliveredAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkFailed_UpdatesStatusAndError()
|
||||
{
|
||||
// Arrange
|
||||
var delivery = CreateDelivery();
|
||||
await _repository.CreateAsync(delivery);
|
||||
|
||||
// Act
|
||||
var result = await _repository.MarkFailedAsync(_tenantId, delivery.Id, "Connection timeout", TimeSpan.FromMinutes(5));
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, delivery.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.Status.Should().Be(DeliveryStatus.Failed);
|
||||
fetched.ErrorMessage.Should().Be("Connection timeout");
|
||||
fetched.FailedAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStats_ReturnsCorrectCounts()
|
||||
{
|
||||
// Arrange
|
||||
var delivery1 = CreateDelivery();
|
||||
var delivery2 = CreateDelivery();
|
||||
await _repository.CreateAsync(delivery1);
|
||||
await _repository.CreateAsync(delivery2);
|
||||
await _repository.MarkSentAsync(_tenantId, delivery2.Id);
|
||||
|
||||
var from = DateTimeOffset.UtcNow.AddHours(-1);
|
||||
var to = DateTimeOffset.UtcNow.AddHours(1);
|
||||
|
||||
// Act
|
||||
var stats = await _repository.GetStatsAsync(_tenantId, from, to);
|
||||
|
||||
// Assert
|
||||
stats.Total.Should().Be(2);
|
||||
stats.Pending.Should().Be(1);
|
||||
stats.Sent.Should().Be(1);
|
||||
}
|
||||
|
||||
private DeliveryEntity CreateDelivery() => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
ChannelId = Guid.NewGuid(),
|
||||
Recipient = "user@example.com",
|
||||
EventType = "scan.completed",
|
||||
Status = DeliveryStatus.Pending
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
using StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(NotifyPostgresCollection.Name)]
|
||||
public sealed class DigestRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NotifyPostgresFixture _fixture;
|
||||
private readonly DigestRepository _repository;
|
||||
private readonly string _tenantId = Guid.NewGuid().ToString();
|
||||
|
||||
public DigestRepositoryTests(NotifyPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
var options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
var dataSource = new NotifyDataSource(Options.Create(options), NullLogger<NotifyDataSource>.Instance);
|
||||
_repository = new DigestRepository(dataSource, NullLogger<DigestRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertAndGetById_RoundTripsDigest()
|
||||
{
|
||||
// Arrange
|
||||
var digest = new DigestEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
ChannelId = Guid.NewGuid(),
|
||||
Recipient = "user@example.com",
|
||||
DigestKey = "daily-summary",
|
||||
EventCount = 0,
|
||||
Status = DigestStatus.Collecting,
|
||||
CollectUntil = DateTimeOffset.UtcNow.AddHours(1)
|
||||
};
|
||||
|
||||
// Act
|
||||
await _repository.UpsertAsync(digest);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, digest.Id);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(digest.Id);
|
||||
fetched.DigestKey.Should().Be("daily-summary");
|
||||
fetched.Status.Should().Be(DigestStatus.Collecting);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByKey_ReturnsCorrectDigest()
|
||||
{
|
||||
// Arrange
|
||||
var channelId = Guid.NewGuid();
|
||||
var digest = new DigestEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
ChannelId = channelId,
|
||||
Recipient = "user@example.com",
|
||||
DigestKey = "weekly-report",
|
||||
CollectUntil = DateTimeOffset.UtcNow.AddDays(7)
|
||||
};
|
||||
await _repository.UpsertAsync(digest);
|
||||
|
||||
// Act
|
||||
var fetched = await _repository.GetByKeyAsync(_tenantId, channelId, "user@example.com", "weekly-report");
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(digest.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddEvent_IncrementsEventCount()
|
||||
{
|
||||
// Arrange
|
||||
var digest = CreateDigest("event-test");
|
||||
await _repository.UpsertAsync(digest);
|
||||
|
||||
// Act
|
||||
await _repository.AddEventAsync(_tenantId, digest.Id, "{\"type\": \"test\"}");
|
||||
await _repository.AddEventAsync(_tenantId, digest.Id, "{\"type\": \"test2\"}");
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, digest.Id);
|
||||
|
||||
// Assert
|
||||
fetched!.EventCount.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReadyToSend_ReturnsDigestsReadyToSend()
|
||||
{
|
||||
// Arrange - One ready digest (past CollectUntil), one not ready
|
||||
var readyDigest = new DigestEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
ChannelId = Guid.NewGuid(),
|
||||
Recipient = "ready@example.com",
|
||||
DigestKey = "ready",
|
||||
Status = DigestStatus.Collecting,
|
||||
CollectUntil = DateTimeOffset.UtcNow.AddMinutes(-1)
|
||||
};
|
||||
var notReadyDigest = new DigestEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
ChannelId = Guid.NewGuid(),
|
||||
Recipient = "notready@example.com",
|
||||
DigestKey = "notready",
|
||||
Status = DigestStatus.Collecting,
|
||||
CollectUntil = DateTimeOffset.UtcNow.AddHours(1)
|
||||
};
|
||||
await _repository.UpsertAsync(readyDigest);
|
||||
await _repository.UpsertAsync(notReadyDigest);
|
||||
|
||||
// Act
|
||||
var readyDigests = await _repository.GetReadyToSendAsync();
|
||||
|
||||
// Assert
|
||||
readyDigests.Should().HaveCount(1);
|
||||
readyDigests[0].DigestKey.Should().Be("ready");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkSending_UpdatesStatus()
|
||||
{
|
||||
// Arrange
|
||||
var digest = CreateDigest("sending-test");
|
||||
await _repository.UpsertAsync(digest);
|
||||
|
||||
// Act
|
||||
var result = await _repository.MarkSendingAsync(_tenantId, digest.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, digest.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.Status.Should().Be(DigestStatus.Sending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkSent_UpdatesStatusAndSentAt()
|
||||
{
|
||||
// Arrange
|
||||
var digest = CreateDigest("sent-test");
|
||||
await _repository.UpsertAsync(digest);
|
||||
await _repository.MarkSendingAsync(_tenantId, digest.Id);
|
||||
|
||||
// Act
|
||||
var result = await _repository.MarkSentAsync(_tenantId, digest.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, digest.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.Status.Should().Be(DigestStatus.Sent);
|
||||
fetched.SentAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteOld_RemovesOldDigests()
|
||||
{
|
||||
// Arrange
|
||||
var digest = CreateDigest("old-digest");
|
||||
await _repository.UpsertAsync(digest);
|
||||
|
||||
// Act - Delete digests older than future date
|
||||
var cutoff = DateTimeOffset.UtcNow.AddMinutes(1);
|
||||
var count = await _repository.DeleteOldAsync(cutoff);
|
||||
|
||||
// Assert
|
||||
count.Should().Be(1);
|
||||
}
|
||||
|
||||
private DigestEntity CreateDigest(string key) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
ChannelId = Guid.NewGuid(),
|
||||
Recipient = "user@example.com",
|
||||
DigestKey = key,
|
||||
Status = DigestStatus.Collecting,
|
||||
CollectUntil = DateTimeOffset.UtcNow.AddHours(1)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
using StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(NotifyPostgresCollection.Name)]
|
||||
public sealed class InboxRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NotifyPostgresFixture _fixture;
|
||||
private readonly InboxRepository _repository;
|
||||
private readonly string _tenantId = Guid.NewGuid().ToString();
|
||||
|
||||
public InboxRepositoryTests(NotifyPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
var options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
var dataSource = new NotifyDataSource(Options.Create(options), NullLogger<NotifyDataSource>.Instance);
|
||||
_repository = new InboxRepository(dataSource, NullLogger<InboxRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndGetById_RoundTripsInboxItem()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var inbox = new InboxEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
UserId = userId,
|
||||
Title = "New Vulnerability Found",
|
||||
Body = "Critical vulnerability CVE-2024-1234 detected",
|
||||
EventType = "vulnerability.found",
|
||||
ActionUrl = "/scans/123/vulnerabilities"
|
||||
};
|
||||
|
||||
// Act
|
||||
await _repository.CreateAsync(inbox);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, inbox.Id);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(inbox.Id);
|
||||
fetched.Title.Should().Be("New Vulnerability Found");
|
||||
fetched.Read.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetForUser_ReturnsUserInboxItems()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var inbox1 = CreateInbox(userId, "Item 1");
|
||||
var inbox2 = CreateInbox(userId, "Item 2");
|
||||
var otherUserInbox = CreateInbox(Guid.NewGuid(), "Other user item");
|
||||
await _repository.CreateAsync(inbox1);
|
||||
await _repository.CreateAsync(inbox2);
|
||||
await _repository.CreateAsync(otherUserInbox);
|
||||
|
||||
// Act
|
||||
var items = await _repository.GetForUserAsync(_tenantId, userId);
|
||||
|
||||
// Assert
|
||||
items.Should().HaveCount(2);
|
||||
items.Select(i => i.Title).Should().Contain(["Item 1", "Item 2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetForUser_FiltersUnreadOnly()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var unreadItem = CreateInbox(userId, "Unread");
|
||||
var readItem = CreateInbox(userId, "Read");
|
||||
await _repository.CreateAsync(unreadItem);
|
||||
await _repository.CreateAsync(readItem);
|
||||
await _repository.MarkReadAsync(_tenantId, readItem.Id);
|
||||
|
||||
// Act
|
||||
var unreadItems = await _repository.GetForUserAsync(_tenantId, userId, unreadOnly: true);
|
||||
|
||||
// Assert
|
||||
unreadItems.Should().HaveCount(1);
|
||||
unreadItems[0].Title.Should().Be("Unread");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUnreadCount_ReturnsCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
await _repository.CreateAsync(CreateInbox(userId, "Unread 1"));
|
||||
await _repository.CreateAsync(CreateInbox(userId, "Unread 2"));
|
||||
var readItem = CreateInbox(userId, "Read");
|
||||
await _repository.CreateAsync(readItem);
|
||||
await _repository.MarkReadAsync(_tenantId, readItem.Id);
|
||||
|
||||
// Act
|
||||
var count = await _repository.GetUnreadCountAsync(_tenantId, userId);
|
||||
|
||||
// Assert
|
||||
count.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkRead_UpdatesReadStatus()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var inbox = CreateInbox(userId, "To be read");
|
||||
await _repository.CreateAsync(inbox);
|
||||
|
||||
// Act
|
||||
var result = await _repository.MarkReadAsync(_tenantId, inbox.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, inbox.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.Read.Should().BeTrue();
|
||||
fetched.ReadAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkAllRead_MarksAllUserItemsAsRead()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
await _repository.CreateAsync(CreateInbox(userId, "Item 1"));
|
||||
await _repository.CreateAsync(CreateInbox(userId, "Item 2"));
|
||||
await _repository.CreateAsync(CreateInbox(userId, "Item 3"));
|
||||
|
||||
// Act
|
||||
var count = await _repository.MarkAllReadAsync(_tenantId, userId);
|
||||
var unreadCount = await _repository.GetUnreadCountAsync(_tenantId, userId);
|
||||
|
||||
// Assert
|
||||
count.Should().Be(3);
|
||||
unreadCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Archive_ArchivesItem()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var inbox = CreateInbox(userId, "To be archived");
|
||||
await _repository.CreateAsync(inbox);
|
||||
|
||||
// Act
|
||||
var result = await _repository.ArchiveAsync(_tenantId, inbox.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, inbox.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.Archived.Should().BeTrue();
|
||||
fetched.ArchivedAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_RemovesItem()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var inbox = CreateInbox(userId, "To be deleted");
|
||||
await _repository.CreateAsync(inbox);
|
||||
|
||||
// Act
|
||||
var result = await _repository.DeleteAsync(_tenantId, inbox.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, inbox.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteOld_RemovesOldItems()
|
||||
{
|
||||
// Arrange - We can't easily set CreatedAt in the test, so this tests the API works
|
||||
var userId = Guid.NewGuid();
|
||||
await _repository.CreateAsync(CreateInbox(userId, "Recent item"));
|
||||
|
||||
// Act - Delete items older than future date (should delete the item)
|
||||
var cutoff = DateTimeOffset.UtcNow.AddMinutes(1);
|
||||
var count = await _repository.DeleteOldAsync(cutoff);
|
||||
|
||||
// Assert
|
||||
count.Should().Be(1);
|
||||
}
|
||||
|
||||
private InboxEntity CreateInbox(Guid userId, string title) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
UserId = userId,
|
||||
Title = title,
|
||||
EventType = "test.event"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
using StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(NotifyPostgresCollection.Name)]
|
||||
public sealed class NotifyAuditRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NotifyPostgresFixture _fixture;
|
||||
private readonly NotifyAuditRepository _repository;
|
||||
private readonly string _tenantId = Guid.NewGuid().ToString();
|
||||
|
||||
public NotifyAuditRepositoryTests(NotifyPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
var options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
var dataSource = new NotifyDataSource(Options.Create(options), NullLogger<NotifyDataSource>.Instance);
|
||||
_repository = new NotifyAuditRepository(dataSource, NullLogger<NotifyAuditRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task Create_ReturnsGeneratedId()
|
||||
{
|
||||
// Arrange
|
||||
var audit = new NotifyAuditEntity
|
||||
{
|
||||
TenantId = _tenantId,
|
||||
UserId = Guid.NewGuid(),
|
||||
Action = "channel.created",
|
||||
ResourceType = "channel",
|
||||
ResourceId = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
// Act
|
||||
var id = await _repository.CreateAsync(audit);
|
||||
|
||||
// Assert
|
||||
id.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_ReturnsAuditEntriesOrderedByCreatedAtDesc()
|
||||
{
|
||||
// Arrange
|
||||
var audit1 = CreateAudit("action1");
|
||||
var audit2 = CreateAudit("action2");
|
||||
await _repository.CreateAsync(audit1);
|
||||
await Task.Delay(10);
|
||||
await _repository.CreateAsync(audit2);
|
||||
|
||||
// Act
|
||||
var audits = await _repository.ListAsync(_tenantId, limit: 10);
|
||||
|
||||
// Assert
|
||||
audits.Should().HaveCount(2);
|
||||
audits[0].Action.Should().Be("action2"); // Most recent first
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByResource_ReturnsResourceAudits()
|
||||
{
|
||||
// Arrange
|
||||
var resourceId = Guid.NewGuid().ToString();
|
||||
var audit = new NotifyAuditEntity
|
||||
{
|
||||
TenantId = _tenantId,
|
||||
Action = "rule.updated",
|
||||
ResourceType = "rule",
|
||||
ResourceId = resourceId
|
||||
};
|
||||
await _repository.CreateAsync(audit);
|
||||
|
||||
// Act
|
||||
var audits = await _repository.GetByResourceAsync(_tenantId, "rule", resourceId);
|
||||
|
||||
// Assert
|
||||
audits.Should().HaveCount(1);
|
||||
audits[0].ResourceId.Should().Be(resourceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByResource_WithoutResourceId_ReturnsAllOfType()
|
||||
{
|
||||
// Arrange
|
||||
await _repository.CreateAsync(new NotifyAuditEntity
|
||||
{
|
||||
TenantId = _tenantId,
|
||||
Action = "template.created",
|
||||
ResourceType = "template",
|
||||
ResourceId = Guid.NewGuid().ToString()
|
||||
});
|
||||
await _repository.CreateAsync(new NotifyAuditEntity
|
||||
{
|
||||
TenantId = _tenantId,
|
||||
Action = "template.updated",
|
||||
ResourceType = "template",
|
||||
ResourceId = Guid.NewGuid().ToString()
|
||||
});
|
||||
|
||||
// Act
|
||||
var audits = await _repository.GetByResourceAsync(_tenantId, "template");
|
||||
|
||||
// Assert
|
||||
audits.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByCorrelationId_ReturnsCorrelatedAudits()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var audit1 = new NotifyAuditEntity
|
||||
{
|
||||
TenantId = _tenantId,
|
||||
Action = "step1",
|
||||
ResourceType = "delivery",
|
||||
CorrelationId = correlationId
|
||||
};
|
||||
var audit2 = new NotifyAuditEntity
|
||||
{
|
||||
TenantId = _tenantId,
|
||||
Action = "step2",
|
||||
ResourceType = "delivery",
|
||||
CorrelationId = correlationId
|
||||
};
|
||||
await _repository.CreateAsync(audit1);
|
||||
await _repository.CreateAsync(audit2);
|
||||
|
||||
// Act
|
||||
var audits = await _repository.GetByCorrelationIdAsync(_tenantId, correlationId);
|
||||
|
||||
// Assert
|
||||
audits.Should().HaveCount(2);
|
||||
audits.Should().AllSatisfy(a => a.CorrelationId.Should().Be(correlationId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteOld_RemovesOldAudits()
|
||||
{
|
||||
// Arrange
|
||||
await _repository.CreateAsync(CreateAudit("old-action"));
|
||||
|
||||
// Act - Delete audits older than future date
|
||||
var cutoff = DateTimeOffset.UtcNow.AddMinutes(1);
|
||||
var count = await _repository.DeleteOldAsync(cutoff);
|
||||
|
||||
// Assert
|
||||
count.Should().Be(1);
|
||||
}
|
||||
|
||||
private NotifyAuditEntity CreateAudit(string action) => new()
|
||||
{
|
||||
TenantId = _tenantId,
|
||||
UserId = Guid.NewGuid(),
|
||||
Action = action,
|
||||
ResourceType = "test",
|
||||
ResourceId = Guid.NewGuid().ToString()
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
using StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(NotifyPostgresCollection.Name)]
|
||||
public sealed class RuleRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NotifyPostgresFixture _fixture;
|
||||
private readonly RuleRepository _repository;
|
||||
private readonly string _tenantId = Guid.NewGuid().ToString();
|
||||
|
||||
public RuleRepositoryTests(NotifyPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
var options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
var dataSource = new NotifyDataSource(Options.Create(options), NullLogger<NotifyDataSource>.Instance);
|
||||
_repository = new RuleRepository(dataSource, NullLogger<RuleRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndGetById_RoundTripsRule()
|
||||
{
|
||||
// Arrange
|
||||
var rule = new RuleEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "critical-alerts",
|
||||
Description = "Send critical alerts to ops team",
|
||||
Enabled = true,
|
||||
Priority = 100,
|
||||
EventTypes = ["scan.completed", "vulnerability.found"],
|
||||
ChannelIds = [Guid.NewGuid()]
|
||||
};
|
||||
|
||||
// Act
|
||||
await _repository.CreateAsync(rule);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, rule.Id);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(rule.Id);
|
||||
fetched.Name.Should().Be("critical-alerts");
|
||||
fetched.Priority.Should().Be(100);
|
||||
fetched.EventTypes.Should().Contain(["scan.completed", "vulnerability.found"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByName_ReturnsCorrectRule()
|
||||
{
|
||||
// Arrange
|
||||
var rule = CreateRule("info-digest");
|
||||
await _repository.CreateAsync(rule);
|
||||
|
||||
// Act
|
||||
var fetched = await _repository.GetByNameAsync(_tenantId, "info-digest");
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(rule.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_ReturnsAllRulesForTenant()
|
||||
{
|
||||
// Arrange
|
||||
var rule1 = CreateRule("rule1");
|
||||
var rule2 = CreateRule("rule2");
|
||||
await _repository.CreateAsync(rule1);
|
||||
await _repository.CreateAsync(rule2);
|
||||
|
||||
// Act
|
||||
var rules = await _repository.ListAsync(_tenantId);
|
||||
|
||||
// Assert
|
||||
rules.Should().HaveCount(2);
|
||||
rules.Select(r => r.Name).Should().Contain(["rule1", "rule2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_FiltersByEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var enabledRule = CreateRule("enabled");
|
||||
var disabledRule = new RuleEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "disabled",
|
||||
Enabled = false,
|
||||
EventTypes = ["test"]
|
||||
};
|
||||
await _repository.CreateAsync(enabledRule);
|
||||
await _repository.CreateAsync(disabledRule);
|
||||
|
||||
// Act
|
||||
var enabledRules = await _repository.ListAsync(_tenantId, enabled: true);
|
||||
|
||||
// Assert
|
||||
enabledRules.Should().HaveCount(1);
|
||||
enabledRules[0].Name.Should().Be("enabled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMatchingRules_ReturnsRulesForEventType()
|
||||
{
|
||||
// Arrange
|
||||
var scanRule = new RuleEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "scan-rule",
|
||||
Enabled = true,
|
||||
EventTypes = ["scan.completed"]
|
||||
};
|
||||
var vulnRule = new RuleEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "vuln-rule",
|
||||
Enabled = true,
|
||||
EventTypes = ["vulnerability.found"]
|
||||
};
|
||||
await _repository.CreateAsync(scanRule);
|
||||
await _repository.CreateAsync(vulnRule);
|
||||
|
||||
// Act
|
||||
var matchingRules = await _repository.GetMatchingRulesAsync(_tenantId, "scan.completed");
|
||||
|
||||
// Assert
|
||||
matchingRules.Should().HaveCount(1);
|
||||
matchingRules[0].Name.Should().Be("scan-rule");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_ModifiesRule()
|
||||
{
|
||||
// Arrange
|
||||
var rule = CreateRule("update-test");
|
||||
await _repository.CreateAsync(rule);
|
||||
|
||||
// Act
|
||||
var updated = new RuleEntity
|
||||
{
|
||||
Id = rule.Id,
|
||||
TenantId = _tenantId,
|
||||
Name = "update-test",
|
||||
Description = "Updated description",
|
||||
Priority = 200,
|
||||
Enabled = false,
|
||||
EventTypes = ["new.event"]
|
||||
};
|
||||
var result = await _repository.UpdateAsync(updated);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, rule.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.Description.Should().Be("Updated description");
|
||||
fetched.Priority.Should().Be(200);
|
||||
fetched.Enabled.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_RemovesRule()
|
||||
{
|
||||
// Arrange
|
||||
var rule = CreateRule("delete-test");
|
||||
await _repository.CreateAsync(rule);
|
||||
|
||||
// Act
|
||||
var result = await _repository.DeleteAsync(_tenantId, rule.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, rule.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched.Should().BeNull();
|
||||
}
|
||||
|
||||
private RuleEntity CreateRule(string name) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = name,
|
||||
Enabled = true,
|
||||
EventTypes = ["test.event"]
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
using StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(NotifyPostgresCollection.Name)]
|
||||
public sealed class TemplateRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NotifyPostgresFixture _fixture;
|
||||
private readonly TemplateRepository _repository;
|
||||
private readonly string _tenantId = Guid.NewGuid().ToString();
|
||||
|
||||
public TemplateRepositoryTests(NotifyPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
|
||||
var options = fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = fixture.SchemaName;
|
||||
var dataSource = new NotifyDataSource(Options.Create(options), NullLogger<NotifyDataSource>.Instance);
|
||||
_repository = new TemplateRepository(dataSource, NullLogger<TemplateRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndGetById_RoundTripsTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var template = new TemplateEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "scan-completed",
|
||||
ChannelType = ChannelType.Email,
|
||||
SubjectTemplate = "Scan Completed: {{imageName}}",
|
||||
BodyTemplate = "<p>Scan for {{imageName}} completed with {{vulnCount}} vulnerabilities.</p>",
|
||||
Locale = "en"
|
||||
};
|
||||
|
||||
// Act
|
||||
await _repository.CreateAsync(template);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, template.Id);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(template.Id);
|
||||
fetched.Name.Should().Be("scan-completed");
|
||||
fetched.ChannelType.Should().Be(ChannelType.Email);
|
||||
fetched.SubjectTemplate.Should().Contain("{{imageName}}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByName_ReturnsCorrectTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var template = CreateTemplate("alert-template", ChannelType.Slack);
|
||||
await _repository.CreateAsync(template);
|
||||
|
||||
// Act
|
||||
var fetched = await _repository.GetByNameAsync(_tenantId, "alert-template", ChannelType.Slack);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(template.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByName_FiltersCorrectlyByLocale()
|
||||
{
|
||||
// Arrange
|
||||
var enTemplate = new TemplateEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "localized-template",
|
||||
ChannelType = ChannelType.Email,
|
||||
BodyTemplate = "English content",
|
||||
Locale = "en"
|
||||
};
|
||||
var frTemplate = new TemplateEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "localized-template",
|
||||
ChannelType = ChannelType.Email,
|
||||
BodyTemplate = "Contenu français",
|
||||
Locale = "fr"
|
||||
};
|
||||
await _repository.CreateAsync(enTemplate);
|
||||
await _repository.CreateAsync(frTemplate);
|
||||
|
||||
// Act
|
||||
var frFetched = await _repository.GetByNameAsync(_tenantId, "localized-template", ChannelType.Email, "fr");
|
||||
|
||||
// Assert
|
||||
frFetched.Should().NotBeNull();
|
||||
frFetched!.BodyTemplate.Should().Contain("français");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_ReturnsAllTemplatesForTenant()
|
||||
{
|
||||
// Arrange
|
||||
var template1 = CreateTemplate("template1", ChannelType.Email);
|
||||
var template2 = CreateTemplate("template2", ChannelType.Slack);
|
||||
await _repository.CreateAsync(template1);
|
||||
await _repository.CreateAsync(template2);
|
||||
|
||||
// Act
|
||||
var templates = await _repository.ListAsync(_tenantId);
|
||||
|
||||
// Assert
|
||||
templates.Should().HaveCount(2);
|
||||
templates.Select(t => t.Name).Should().Contain(["template1", "template2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_FiltersByChannelType()
|
||||
{
|
||||
// Arrange
|
||||
var emailTemplate = CreateTemplate("email", ChannelType.Email);
|
||||
var slackTemplate = CreateTemplate("slack", ChannelType.Slack);
|
||||
await _repository.CreateAsync(emailTemplate);
|
||||
await _repository.CreateAsync(slackTemplate);
|
||||
|
||||
// Act
|
||||
var emailTemplates = await _repository.ListAsync(_tenantId, channelType: ChannelType.Email);
|
||||
|
||||
// Assert
|
||||
emailTemplates.Should().HaveCount(1);
|
||||
emailTemplates[0].Name.Should().Be("email");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_ModifiesTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var template = CreateTemplate("update-test", ChannelType.Email);
|
||||
await _repository.CreateAsync(template);
|
||||
|
||||
// Act
|
||||
var updated = new TemplateEntity
|
||||
{
|
||||
Id = template.Id,
|
||||
TenantId = _tenantId,
|
||||
Name = "update-test",
|
||||
ChannelType = ChannelType.Email,
|
||||
SubjectTemplate = "Updated Subject",
|
||||
BodyTemplate = "Updated body content"
|
||||
};
|
||||
var result = await _repository.UpdateAsync(updated);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, template.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.SubjectTemplate.Should().Be("Updated Subject");
|
||||
fetched.BodyTemplate.Should().Be("Updated body content");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_RemovesTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var template = CreateTemplate("delete-test", ChannelType.Email);
|
||||
await _repository.CreateAsync(template);
|
||||
|
||||
// Act
|
||||
var result = await _repository.DeleteAsync(_tenantId, template.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, template.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched.Should().BeNull();
|
||||
}
|
||||
|
||||
private TemplateEntity CreateTemplate(string name, ChannelType type) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = name,
|
||||
ChannelType = type,
|
||||
BodyTemplate = "Default template body"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user