diff --git a/src/Policy/__Tests/StellaOps.Policy.Storage.Postgres.Tests/PostgresExceptionApplicationRepositoryTests.cs b/src/Policy/__Tests/StellaOps.Policy.Storage.Postgres.Tests/PostgresExceptionApplicationRepositoryTests.cs new file mode 100644 index 000000000..0fb680f1d --- /dev/null +++ b/src/Policy/__Tests/StellaOps.Policy.Storage.Postgres.Tests/PostgresExceptionApplicationRepositoryTests.cs @@ -0,0 +1,95 @@ +using System.Collections.Immutable; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StellaOps.Policy.Exceptions.Models; +using StellaOps.Policy.Exceptions.Repositories; +using Xunit; + +namespace StellaOps.Policy.Storage.Postgres.Tests; + +[Collection(PolicyPostgresCollection.Name)] +public sealed class PostgresExceptionApplicationRepositoryTests : IAsyncLifetime +{ + private readonly PolicyPostgresFixture _fixture; + private readonly PostgresExceptionApplicationRepository _repository; + private readonly Guid _tenantId = Guid.NewGuid(); + + public PostgresExceptionApplicationRepositoryTests(PolicyPostgresFixture fixture) + { + _fixture = fixture; + var options = fixture.Fixture.CreateOptions(); + options.SchemaName = fixture.SchemaName; + var dataSource = new PolicyDataSource(Options.Create(options), NullLogger.Instance); + _repository = new PostgresExceptionApplicationRepository(dataSource.DataSource); + } + + public Task InitializeAsync() => _fixture.TruncateAllTablesAsync(); + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public async Task RecordAsync_ShouldPersist() + { + var app = CreateApp("EXC-001", "FIND-001"); + var result = await _repository.RecordAsync(app); + result.Should().NotBeNull(); + result.ExceptionId.Should().Be("EXC-001"); + } + + [Fact] + public async Task RecordBatchAsync_ShouldPersistMultiple() + { + var apps = new[] { CreateApp("EXC-B1", "F1"), CreateApp("EXC-B1", "F2"), CreateApp("EXC-B2", "F3") }; + var result = await _repository.RecordBatchAsync(apps); + result.Should().HaveCount(3); + } + + [Fact] + public async Task RecordBatchAsync_EmptyReturnsEmpty() + { + var result = await _repository.RecordBatchAsync(Array.Empty()); + result.Should().BeEmpty(); + } + + [Fact] + public async Task GetByExceptionIdAsync_ReturnsMatches() + { + await _repository.RecordAsync(CreateApp("EXC-G1", "F1")); + await _repository.RecordAsync(CreateApp("EXC-G1", "F2")); + await _repository.RecordAsync(CreateApp("EXC-OTH", "F3")); + var results = await _repository.GetByExceptionIdAsync(_tenantId, "EXC-G1"); + results.Should().HaveCount(2); + } + + [Fact] + public async Task GetByFindingIdAsync_ReturnsMatches() + { + await _repository.RecordAsync(CreateApp("E1", "FIND-G1")); + await _repository.RecordAsync(CreateApp("E2", "FIND-G1")); + await _repository.RecordAsync(CreateApp("E3", "FIND-OTH")); + var results = await _repository.GetByFindingIdAsync(_tenantId, "FIND-G1"); + results.Should().HaveCount(2); + } + + [Fact] + public async Task GetByVulnerabilityIdAsync_ReturnsMatches() + { + await _repository.RecordAsync(CreateApp("E1", "F1", "CVE-2024-1234")); + await _repository.RecordAsync(CreateApp("E2", "F2", "CVE-2024-1234")); + await _repository.RecordAsync(CreateApp("E3", "F3", "CVE-2024-5678")); + var results = await _repository.GetByVulnerabilityIdAsync(_tenantId, "CVE-2024-1234"); + results.Should().HaveCount(2); + } + + [Fact] + public async Task CountAsync_WithFilter_ReturnsFiltered() + { + await _repository.RecordBatchAsync(new[] { CreateApp("E1", "F1", eff: "suppress"), CreateApp("E2", "F2", eff: "suppress"), CreateApp("E3", "F3", eff: "modify") }); + var filter = new ExceptionApplicationFilter(EffectType: "suppress"); + var count = await _repository.CountAsync(_tenantId, filter); + count.Should().Be(2); + } + + private ExceptionApplication CreateApp(string excId, string findId, string? vulnId = null, string eff = "suppress") => + ExceptionApplication.Create(_tenantId, excId, findId, "affected", "not_affected", "test", eff, vulnId); +} \ No newline at end of file