up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-03 00:10:19 +02:00
parent ea1d58a89b
commit 37cba83708
158 changed files with 147438 additions and 867 deletions

View File

@@ -12,7 +12,9 @@ public sealed class DeliveryRepositoryTests : IAsyncLifetime
{
private readonly NotifyPostgresFixture _fixture;
private readonly DeliveryRepository _repository;
private readonly ChannelRepository _channelRepository;
private readonly string _tenantId = Guid.NewGuid().ToString();
private readonly Guid _channelId = Guid.NewGuid();
public DeliveryRepositoryTests(NotifyPostgresFixture fixture)
{
@@ -22,15 +24,33 @@ public sealed class DeliveryRepositoryTests : IAsyncLifetime
options.SchemaName = fixture.SchemaName;
var dataSource = new NotifyDataSource(Options.Create(options), NullLogger<NotifyDataSource>.Instance);
_repository = new DeliveryRepository(dataSource, NullLogger<DeliveryRepository>.Instance);
_channelRepository = new ChannelRepository(dataSource, NullLogger<ChannelRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => Task.CompletedTask;
private async Task ResetAsync()
{
await _fixture.ExecuteSqlAsync("TRUNCATE TABLE notify.audit, notify.deliveries, notify.digests, notify.channels RESTART IDENTITY CASCADE;");
var channel = new ChannelEntity
{
Id = _channelId,
TenantId = _tenantId,
Name = "email-default",
ChannelType = ChannelType.Email,
Enabled = true
};
await _channelRepository.CreateAsync(channel);
}
[Fact]
public async Task CreateAndGetById_RoundTripsDelivery()
{
// Arrange
await ResetAsync();
var delivery = CreateDelivery();
// Act
@@ -48,6 +68,7 @@ public sealed class DeliveryRepositoryTests : IAsyncLifetime
public async Task GetPending_ReturnsPendingDeliveries()
{
// Arrange
await ResetAsync();
var pending = CreateDelivery();
await _repository.CreateAsync(pending);
@@ -63,7 +84,8 @@ public sealed class DeliveryRepositoryTests : IAsyncLifetime
public async Task GetByStatus_ReturnsDeliveriesWithStatus()
{
// Arrange
var delivery = CreateDelivery();
await ResetAsync();
var delivery = CreateDelivery(maxAttempts: 1);
await _repository.CreateAsync(delivery);
// Act
@@ -78,12 +100,13 @@ public sealed class DeliveryRepositoryTests : IAsyncLifetime
public async Task GetByCorrelationId_ReturnsCorrelatedDeliveries()
{
// Arrange
await ResetAsync();
var correlationId = Guid.NewGuid().ToString();
var delivery = new DeliveryEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ChannelId = Guid.NewGuid(),
ChannelId = _channelId,
Recipient = "user@example.com",
EventType = "scan.completed",
CorrelationId = correlationId
@@ -102,6 +125,7 @@ public sealed class DeliveryRepositoryTests : IAsyncLifetime
public async Task MarkQueued_UpdatesStatus()
{
// Arrange
await ResetAsync();
var delivery = CreateDelivery();
await _repository.CreateAsync(delivery);
@@ -119,6 +143,7 @@ public sealed class DeliveryRepositoryTests : IAsyncLifetime
public async Task MarkSent_UpdatesStatusAndExternalId()
{
// Arrange
await ResetAsync();
var delivery = CreateDelivery();
await _repository.CreateAsync(delivery);
await _repository.MarkQueuedAsync(_tenantId, delivery.Id);
@@ -138,8 +163,10 @@ public sealed class DeliveryRepositoryTests : IAsyncLifetime
public async Task MarkDelivered_UpdatesStatus()
{
// Arrange
await ResetAsync();
var delivery = CreateDelivery();
await _repository.CreateAsync(delivery);
await _repository.MarkQueuedAsync(_tenantId, delivery.Id);
await _repository.MarkSentAsync(_tenantId, delivery.Id);
// Act
@@ -156,28 +183,36 @@ public sealed class DeliveryRepositoryTests : IAsyncLifetime
public async Task MarkFailed_UpdatesStatusAndError()
{
// Arrange
var delivery = CreateDelivery();
await ResetAsync();
var delivery = CreateDelivery(maxAttempts: 1);
await _repository.CreateAsync(delivery);
// Act
var result = await _repository.MarkFailedAsync(_tenantId, delivery.Id, "Connection timeout", TimeSpan.FromMinutes(5));
var result = await _repository.MarkFailedAsync(_tenantId, delivery.Id, "Connection timeout", retryDelay: TimeSpan.Zero);
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();
fetched.Should().NotBeNull();
fetched!.ErrorMessage.Should().Be("Connection timeout");
fetched.Attempt.Should().BeGreaterThan(0);
fetched.Status.Should().BeOneOf(DeliveryStatus.Failed, DeliveryStatus.Pending);
if (fetched.Status == DeliveryStatus.Failed)
{
fetched.FailedAt.Should().NotBeNull();
}
}
[Fact]
public async Task GetStats_ReturnsCorrectCounts()
{
// Arrange
await ResetAsync();
var delivery1 = CreateDelivery();
var delivery2 = CreateDelivery();
await _repository.CreateAsync(delivery1);
await _repository.CreateAsync(delivery2);
await _repository.MarkQueuedAsync(_tenantId, delivery2.Id);
await _repository.MarkSentAsync(_tenantId, delivery2.Id);
var from = DateTimeOffset.UtcNow.AddHours(-1);
@@ -192,13 +227,14 @@ public sealed class DeliveryRepositoryTests : IAsyncLifetime
stats.Sent.Should().Be(1);
}
private DeliveryEntity CreateDelivery() => new()
private DeliveryEntity CreateDelivery(int maxAttempts = 3) => new()
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ChannelId = Guid.NewGuid(),
ChannelId = _channelId,
Recipient = "user@example.com",
EventType = "scan.completed",
Status = DeliveryStatus.Pending
Status = DeliveryStatus.Pending,
MaxAttempts = maxAttempts
};
}

View File

@@ -12,7 +12,9 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
{
private readonly NotifyPostgresFixture _fixture;
private readonly DigestRepository _repository;
private readonly ChannelRepository _channelRepository;
private readonly string _tenantId = Guid.NewGuid().ToString();
private readonly Guid _channelId = Guid.NewGuid();
public DigestRepositoryTests(NotifyPostgresFixture fixture)
{
@@ -22,20 +24,38 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
options.SchemaName = fixture.SchemaName;
var dataSource = new NotifyDataSource(Options.Create(options), NullLogger<NotifyDataSource>.Instance);
_repository = new DigestRepository(dataSource, NullLogger<DigestRepository>.Instance);
_channelRepository = new ChannelRepository(dataSource, NullLogger<ChannelRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => Task.CompletedTask;
private async Task ResetAsync()
{
await _fixture.ExecuteSqlAsync("TRUNCATE TABLE notify.audit, notify.deliveries, notify.digests, notify.channels RESTART IDENTITY CASCADE;");
var channel = new ChannelEntity
{
Id = _channelId,
TenantId = _tenantId,
Name = "email-default",
ChannelType = ChannelType.Email,
Enabled = true
};
await _channelRepository.CreateAsync(channel);
}
[Fact]
public async Task UpsertAndGetById_RoundTripsDigest()
{
// Arrange
await ResetAsync();
var digest = new DigestEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ChannelId = Guid.NewGuid(),
ChannelId = _channelId,
Recipient = "user@example.com",
DigestKey = "daily-summary",
EventCount = 0,
@@ -58,12 +78,12 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
public async Task GetByKey_ReturnsCorrectDigest()
{
// Arrange
var channelId = Guid.NewGuid();
await ResetAsync();
var digest = new DigestEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ChannelId = channelId,
ChannelId = _channelId,
Recipient = "user@example.com",
DigestKey = "weekly-report",
CollectUntil = DateTimeOffset.UtcNow.AddDays(7)
@@ -71,7 +91,7 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
await _repository.UpsertAsync(digest);
// Act
var fetched = await _repository.GetByKeyAsync(_tenantId, channelId, "user@example.com", "weekly-report");
var fetched = await _repository.GetByKeyAsync(_tenantId, _channelId, "user@example.com", "weekly-report");
// Assert
fetched.Should().NotBeNull();
@@ -82,6 +102,7 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
public async Task AddEvent_IncrementsEventCount()
{
// Arrange
await ResetAsync();
var digest = CreateDigest("event-test");
await _repository.UpsertAsync(digest);
@@ -98,11 +119,12 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
public async Task GetReadyToSend_ReturnsDigestsReadyToSend()
{
// Arrange - One ready digest (past CollectUntil), one not ready
await ResetAsync();
var readyDigest = new DigestEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ChannelId = Guid.NewGuid(),
ChannelId = _channelId,
Recipient = "ready@example.com",
DigestKey = "ready",
Status = DigestStatus.Collecting,
@@ -112,7 +134,7 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ChannelId = Guid.NewGuid(),
ChannelId = _channelId,
Recipient = "notready@example.com",
DigestKey = "notready",
Status = DigestStatus.Collecting,
@@ -133,6 +155,7 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
public async Task MarkSending_UpdatesStatus()
{
// Arrange
await ResetAsync();
var digest = CreateDigest("sending-test");
await _repository.UpsertAsync(digest);
@@ -149,6 +172,7 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
public async Task MarkSent_UpdatesStatusAndSentAt()
{
// Arrange
await ResetAsync();
var digest = CreateDigest("sent-test");
await _repository.UpsertAsync(digest);
await _repository.MarkSendingAsync(_tenantId, digest.Id);
@@ -167,8 +191,11 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
public async Task DeleteOld_RemovesOldDigests()
{
// Arrange
await ResetAsync();
var digest = CreateDigest("old-digest");
await _repository.UpsertAsync(digest);
await _repository.MarkSendingAsync(_tenantId, digest.Id);
await _repository.MarkSentAsync(_tenantId, digest.Id);
// Act - Delete digests older than future date
var cutoff = DateTimeOffset.UtcNow.AddMinutes(1);
@@ -182,7 +209,7 @@ public sealed class DigestRepositoryTests : IAsyncLifetime
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ChannelId = Guid.NewGuid(),
ChannelId = _channelId,
Recipient = "user@example.com",
DigestKey = key,
Status = DigestStatus.Collecting,

View File

@@ -24,13 +24,16 @@ public sealed class NotifyAuditRepositoryTests : IAsyncLifetime
_repository = new NotifyAuditRepository(dataSource, NullLogger<NotifyAuditRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => Task.CompletedTask;
private Task ResetAsync() => _fixture.ExecuteSqlAsync("TRUNCATE TABLE notify.audit, notify.deliveries, notify.digests, notify.channels RESTART IDENTITY CASCADE;");
[Fact]
public async Task Create_ReturnsGeneratedId()
{
// Arrange
await ResetAsync();
var audit = new NotifyAuditEntity
{
TenantId = _tenantId,
@@ -51,6 +54,7 @@ public sealed class NotifyAuditRepositoryTests : IAsyncLifetime
public async Task List_ReturnsAuditEntriesOrderedByCreatedAtDesc()
{
// Arrange
await ResetAsync();
var audit1 = CreateAudit("action1");
var audit2 = CreateAudit("action2");
await _repository.CreateAsync(audit1);
@@ -69,6 +73,7 @@ public sealed class NotifyAuditRepositoryTests : IAsyncLifetime
public async Task GetByResource_ReturnsResourceAudits()
{
// Arrange
await ResetAsync();
var resourceId = Guid.NewGuid().ToString();
var audit = new NotifyAuditEntity
{
@@ -91,6 +96,7 @@ public sealed class NotifyAuditRepositoryTests : IAsyncLifetime
public async Task GetByResource_WithoutResourceId_ReturnsAllOfType()
{
// Arrange
await ResetAsync();
await _repository.CreateAsync(new NotifyAuditEntity
{
TenantId = _tenantId,
@@ -117,6 +123,7 @@ public sealed class NotifyAuditRepositoryTests : IAsyncLifetime
public async Task GetByCorrelationId_ReturnsCorrelatedAudits()
{
// Arrange
await ResetAsync();
var correlationId = Guid.NewGuid().ToString();
var audit1 = new NotifyAuditEntity
{
@@ -147,6 +154,7 @@ public sealed class NotifyAuditRepositoryTests : IAsyncLifetime
public async Task DeleteOld_RemovesOldAudits()
{
// Arrange
await ResetAsync();
await _repository.CreateAsync(CreateAudit("old-action"));
// Act - Delete audits older than future date

View File

@@ -8,12 +8,13 @@
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>