292 lines
9.4 KiB
C#
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
|
|
};
|
|
}
|