Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AirGap.Controller.Domain;
|
||||
using StellaOps.AirGap.Persistence.Postgres;
|
||||
using StellaOps.AirGap.Persistence.Postgres.Repositories;
|
||||
using StellaOps.AirGap.Time.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Persistence.Tests;
|
||||
|
||||
[Collection(AirGapPostgresCollection.Name)]
|
||||
public sealed class PostgresAirGapStateStoreTests : IAsyncLifetime
|
||||
{
|
||||
private readonly AirGapPostgresFixture _fixture;
|
||||
private readonly PostgresAirGapStateStore _store;
|
||||
private readonly AirGapDataSource _dataSource;
|
||||
private readonly string _tenantId = "tenant-" + Guid.NewGuid().ToString("N")[..8];
|
||||
|
||||
public PostgresAirGapStateStoreTests(AirGapPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
var options = Options.Create(new PostgresOptions
|
||||
{
|
||||
ConnectionString = fixture.ConnectionString,
|
||||
SchemaName = AirGapDataSource.DefaultSchemaName,
|
||||
AutoMigrate = false
|
||||
});
|
||||
|
||||
_dataSource = new AirGapDataSource(options, NullLogger<AirGapDataSource>.Instance);
|
||||
_store = new PostgresAirGapStateStore(_dataSource, NullLogger<PostgresAirGapStateStore>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAsync_ReturnsDefaultStateForNewTenant()
|
||||
{
|
||||
// Act
|
||||
var state = await _store.GetAsync(_tenantId);
|
||||
|
||||
// Assert
|
||||
state.Should().NotBeNull();
|
||||
state.TenantId.Should().Be(_tenantId);
|
||||
state.Sealed.Should().BeFalse();
|
||||
state.PolicyHash.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetAndGet_RoundTripsState()
|
||||
{
|
||||
// Arrange
|
||||
var timeAnchor = new TimeAnchor(
|
||||
DateTimeOffset.UtcNow,
|
||||
"tsa.example.com",
|
||||
"RFC3161",
|
||||
"sha256:fingerprint123",
|
||||
"sha256:tokendigest456");
|
||||
|
||||
var state = new AirGapState
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
TenantId = _tenantId,
|
||||
Sealed = true,
|
||||
PolicyHash = "sha256:policy789",
|
||||
TimeAnchor = timeAnchor,
|
||||
LastTransitionAt = DateTimeOffset.UtcNow,
|
||||
StalenessBudget = new StalenessBudget(1800, 3600),
|
||||
DriftBaselineSeconds = 5,
|
||||
ContentBudgets = new Dictionary<string, StalenessBudget>
|
||||
{
|
||||
["advisories"] = new StalenessBudget(7200, 14400),
|
||||
["vex"] = new StalenessBudget(3600, 7200)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await _store.SetAsync(state);
|
||||
var fetched = await _store.GetAsync(_tenantId);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched.Sealed.Should().BeTrue();
|
||||
fetched.PolicyHash.Should().Be("sha256:policy789");
|
||||
fetched.TimeAnchor.Source.Should().Be("tsa.example.com");
|
||||
fetched.TimeAnchor.Format.Should().Be("RFC3161");
|
||||
fetched.StalenessBudget.WarningSeconds.Should().Be(1800);
|
||||
fetched.StalenessBudget.BreachSeconds.Should().Be(3600);
|
||||
fetched.DriftBaselineSeconds.Should().Be(5);
|
||||
fetched.ContentBudgets.Should().HaveCount(2);
|
||||
fetched.ContentBudgets["advisories"].WarningSeconds.Should().Be(7200);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetAsync_UpdatesExistingState()
|
||||
{
|
||||
// Arrange
|
||||
var state1 = new AirGapState
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
TenantId = _tenantId,
|
||||
Sealed = false,
|
||||
TimeAnchor = TimeAnchor.Unknown,
|
||||
StalenessBudget = StalenessBudget.Default
|
||||
};
|
||||
|
||||
var state2 = new AirGapState
|
||||
{
|
||||
Id = state1.Id,
|
||||
TenantId = _tenantId,
|
||||
Sealed = true,
|
||||
PolicyHash = "sha256:updated",
|
||||
TimeAnchor = new TimeAnchor(DateTimeOffset.UtcNow, "updated-source", "rfc3161", "", ""),
|
||||
LastTransitionAt = DateTimeOffset.UtcNow,
|
||||
StalenessBudget = new StalenessBudget(600, 1200)
|
||||
};
|
||||
|
||||
// Act
|
||||
await _store.SetAsync(state1);
|
||||
await _store.SetAsync(state2);
|
||||
var fetched = await _store.GetAsync(_tenantId);
|
||||
|
||||
// Assert
|
||||
fetched.Sealed.Should().BeTrue();
|
||||
fetched.PolicyHash.Should().Be("sha256:updated");
|
||||
fetched.TimeAnchor.Source.Should().Be("updated-source");
|
||||
fetched.StalenessBudget.WarningSeconds.Should().Be(600);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetAsync_PersistsContentBudgets()
|
||||
{
|
||||
// Arrange
|
||||
var state = new AirGapState
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
TenantId = _tenantId,
|
||||
TimeAnchor = TimeAnchor.Unknown,
|
||||
StalenessBudget = StalenessBudget.Default,
|
||||
ContentBudgets = new Dictionary<string, StalenessBudget>
|
||||
{
|
||||
["advisories"] = new StalenessBudget(3600, 7200),
|
||||
["vex"] = new StalenessBudget(1800, 3600),
|
||||
["policy"] = new StalenessBudget(900, 1800)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
await _store.SetAsync(state);
|
||||
var fetched = await _store.GetAsync(_tenantId);
|
||||
|
||||
// Assert
|
||||
fetched.ContentBudgets.Should().HaveCount(3);
|
||||
fetched.ContentBudgets.Should().ContainKey("advisories");
|
||||
fetched.ContentBudgets.Should().ContainKey("vex");
|
||||
fetched.ContentBudgets.Should().ContainKey("policy");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user