Files
git.stella-ops.org/src/Policy/__Tests/StellaOps.Policy.Persistence.Tests/ExceptionRepositoryTests.cs

292 lines
9.4 KiB
C#

using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Persistence.Postgres;
using StellaOps.Policy.Persistence.Postgres.Models;
using StellaOps.Policy.Persistence.Postgres.Repositories;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Policy.Persistence.Tests;
[Collection(PolicyPostgresCollection.Name)]
public sealed class ExceptionRepositoryTests : IAsyncLifetime
{
private readonly PolicyPostgresFixture _fixture;
private readonly ExceptionRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public ExceptionRepositoryTests(PolicyPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new PolicyDataSource(Options.Create(options), NullLogger<PolicyDataSource>.Instance);
_repository = new ExceptionRepository(dataSource, NullLogger<ExceptionRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CreateAndGetById_RoundTripsException()
{
// Arrange
var exception = new ExceptionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "legacy-root-container",
Description = "Allow root containers for legacy app",
RulePattern = "no-root-containers",
ProjectId = "project-legacy",
Reason = "Legacy application requires root access",
Status = ExceptionStatus.Active,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30)
};
// Act
await _repository.CreateAsync(exception);
var fetched = await _repository.GetByIdAsync(_tenantId, exception.Id);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(exception.Id);
fetched.Name.Should().Be("legacy-root-container");
fetched.Status.Should().Be(ExceptionStatus.Active);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetByName_ReturnsCorrectException()
{
// Arrange
var exception = CreateException("temp-waiver");
await _repository.CreateAsync(exception);
// Act
var fetched = await _repository.GetByNameAsync(_tenantId, "temp-waiver");
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(exception.Id);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetAll_ReturnsAllExceptionsForTenant()
{
// Arrange
var exception1 = CreateException("exception1");
var exception2 = CreateException("exception2");
await _repository.CreateAsync(exception1);
await _repository.CreateAsync(exception2);
// Act
var exceptions = await _repository.GetAllAsync(_tenantId);
// Assert
exceptions.Should().HaveCount(2);
exceptions.Select(e => e.Name).Should().Contain(["exception1", "exception2"]);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetAll_FiltersByStatus()
{
// Arrange
var activeException = CreateException("active");
var revokedException = new ExceptionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "revoked",
Reason = "Test",
Status = ExceptionStatus.Revoked
};
await _repository.CreateAsync(activeException);
await _repository.CreateAsync(revokedException);
// Act
var activeExceptions = await _repository.GetAllAsync(_tenantId, status: ExceptionStatus.Active);
// Assert
activeExceptions.Should().HaveCount(1);
activeExceptions[0].Name.Should().Be("active");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetActiveForProject_ReturnsProjectExceptions()
{
// Arrange
var projectException = new ExceptionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "project-exception",
ProjectId = "project-123",
Reason = "Project-specific waiver",
Status = ExceptionStatus.Active
};
var otherProjectException = new ExceptionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "other-exception",
ProjectId = "project-456",
Reason = "Other project waiver",
Status = ExceptionStatus.Active
};
await _repository.CreateAsync(projectException);
await _repository.CreateAsync(otherProjectException);
// Act
var exceptions = await _repository.GetActiveForProjectAsync(_tenantId, "project-123");
// Assert
exceptions.Should().HaveCount(1);
exceptions[0].Name.Should().Be("project-exception");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetActiveForRule_ReturnsRuleExceptions()
{
// Arrange
var ruleException = new ExceptionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "rule-exception",
RulePattern = "no-root-containers",
Reason = "Rule-specific waiver",
Status = ExceptionStatus.Active
};
await _repository.CreateAsync(ruleException);
// Act
var exceptions = await _repository.GetActiveForRuleAsync(_tenantId, "no-root-containers");
// Assert
exceptions.Should().HaveCount(1);
exceptions[0].Name.Should().Be("rule-exception");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Update_ModifiesException()
{
// Arrange
var exception = CreateException("update-test");
await _repository.CreateAsync(exception);
// Act
var updated = new ExceptionEntity
{
Id = exception.Id,
TenantId = _tenantId,
Name = "update-test",
Reason = "Updated reason",
Description = "Updated description"
};
var result = await _repository.UpdateAsync(updated);
var fetched = await _repository.GetByIdAsync(_tenantId, exception.Id);
// Assert
result.Should().BeTrue();
fetched!.Reason.Should().Be("Updated reason");
fetched.Description.Should().Be("Updated description");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Approve_SetsApprovalDetails()
{
// Arrange
var exception = CreateException("approve-test");
await _repository.CreateAsync(exception);
// Act
var result = await _repository.ApproveAsync(_tenantId, exception.Id, "admin@example.com");
var fetched = await _repository.GetByIdAsync(_tenantId, exception.Id);
// Assert
result.Should().BeTrue();
fetched!.ApprovedBy.Should().Be("admin@example.com");
fetched.ApprovedAt.Should().NotBeNull();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Revoke_SetsRevokedStatusAndDetails()
{
// Arrange
var exception = CreateException("revoke-test");
await _repository.CreateAsync(exception);
// Act
var result = await _repository.RevokeAsync(_tenantId, exception.Id, "admin@example.com");
var fetched = await _repository.GetByIdAsync(_tenantId, exception.Id);
// Assert
result.Should().BeTrue();
fetched!.Status.Should().Be(ExceptionStatus.Revoked);
fetched.RevokedBy.Should().Be("admin@example.com");
fetched.RevokedAt.Should().NotBeNull();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Expire_ExpiresOldExceptions()
{
// Arrange - Create an exception that expires in the past
var expiredException = new ExceptionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "expired",
Reason = "Test",
Status = ExceptionStatus.Active,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(-1)
};
await _repository.CreateAsync(expiredException);
// Act
var count = await _repository.ExpireAsync(_tenantId);
var fetched = await _repository.GetByIdAsync(_tenantId, expiredException.Id);
// Assert
count.Should().Be(1);
fetched!.Status.Should().Be(ExceptionStatus.Expired);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Delete_RemovesException()
{
// Arrange
var exception = CreateException("delete-test");
await _repository.CreateAsync(exception);
// Act
var result = await _repository.DeleteAsync(_tenantId, exception.Id);
var fetched = await _repository.GetByIdAsync(_tenantId, exception.Id);
// Assert
result.Should().BeTrue();
fetched.Should().BeNull();
}
private ExceptionEntity CreateException(string name) => new()
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = name,
Reason = "Test exception",
Status = ExceptionStatus.Active
};
}