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,362 @@
|
||||
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 RiskProfileRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly PolicyPostgresFixture _fixture;
|
||||
private readonly RiskProfileRepository _repository;
|
||||
private readonly string _tenantId = Guid.NewGuid().ToString();
|
||||
|
||||
public RiskProfileRepositoryTests(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 RiskProfileRepository(dataSource, NullLogger<RiskProfileRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAndGetById_RoundTripsRiskProfile()
|
||||
{
|
||||
// Arrange
|
||||
var profile = new RiskProfileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "default",
|
||||
DisplayName = "Default Risk Profile",
|
||||
Description = "Standard risk scoring profile",
|
||||
Version = 1,
|
||||
IsActive = true,
|
||||
Thresholds = "{\"critical\": 9.0, \"high\": 7.0}",
|
||||
ScoringWeights = "{\"vulnerability\": 1.0, \"configuration\": 0.5}"
|
||||
};
|
||||
|
||||
// Act
|
||||
await _repository.CreateAsync(profile);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, profile.Id);
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Id.Should().Be(profile.Id);
|
||||
fetched.Name.Should().Be("default");
|
||||
fetched.Version.Should().Be(1);
|
||||
fetched.IsActive.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetActiveByName_ReturnsActiveVersion()
|
||||
{
|
||||
// Arrange
|
||||
var inactiveProfile = new RiskProfileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "versioned-profile",
|
||||
Version = 1,
|
||||
IsActive = false
|
||||
};
|
||||
var activeProfile = new RiskProfileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "versioned-profile",
|
||||
Version = 2,
|
||||
IsActive = true
|
||||
};
|
||||
await _repository.CreateAsync(inactiveProfile);
|
||||
await _repository.CreateAsync(activeProfile);
|
||||
|
||||
// Act
|
||||
var fetched = await _repository.GetActiveByNameAsync(_tenantId, "versioned-profile");
|
||||
|
||||
// Assert
|
||||
fetched.Should().NotBeNull();
|
||||
fetched!.Version.Should().Be(2);
|
||||
fetched.IsActive.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAll_ReturnsProfilesForTenant()
|
||||
{
|
||||
// Arrange
|
||||
var profile1 = CreateProfile("profile1");
|
||||
var profile2 = CreateProfile("profile2");
|
||||
await _repository.CreateAsync(profile1);
|
||||
await _repository.CreateAsync(profile2);
|
||||
|
||||
// Act
|
||||
var profiles = await _repository.GetAllAsync(_tenantId);
|
||||
|
||||
// Assert
|
||||
profiles.Should().HaveCount(2);
|
||||
profiles.Select(p => p.Name).Should().Contain(["profile1", "profile2"]);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAll_FiltersActiveOnly()
|
||||
{
|
||||
// Arrange
|
||||
var activeProfile = CreateProfile("active");
|
||||
var inactiveProfile = new RiskProfileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "inactive",
|
||||
IsActive = false
|
||||
};
|
||||
await _repository.CreateAsync(activeProfile);
|
||||
await _repository.CreateAsync(inactiveProfile);
|
||||
|
||||
// Act
|
||||
var activeProfiles = await _repository.GetAllAsync(_tenantId, activeOnly: true);
|
||||
|
||||
// Assert
|
||||
activeProfiles.Should().HaveCount(1);
|
||||
activeProfiles[0].Name.Should().Be("active");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetVersionsByName_ReturnsAllVersions()
|
||||
{
|
||||
// Arrange
|
||||
var v1 = new RiskProfileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "multi-version",
|
||||
Version = 1,
|
||||
IsActive = false
|
||||
};
|
||||
var v2 = new RiskProfileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "multi-version",
|
||||
Version = 2,
|
||||
IsActive = true
|
||||
};
|
||||
await _repository.CreateAsync(v1);
|
||||
await _repository.CreateAsync(v2);
|
||||
|
||||
// Act
|
||||
var versions = await _repository.GetVersionsByNameAsync(_tenantId, "multi-version");
|
||||
|
||||
// Assert
|
||||
versions.Should().HaveCount(2);
|
||||
versions.Select(v => v.Version).Should().Contain([1, 2]);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Update_ModifiesProfile()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateProfile("update-test");
|
||||
await _repository.CreateAsync(profile);
|
||||
|
||||
// Act
|
||||
var updated = new RiskProfileEntity
|
||||
{
|
||||
Id = profile.Id,
|
||||
TenantId = _tenantId,
|
||||
Name = "update-test",
|
||||
DisplayName = "Updated Display Name",
|
||||
Description = "Updated description",
|
||||
Thresholds = "{\"critical\": 8.0}"
|
||||
};
|
||||
var result = await _repository.UpdateAsync(updated);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, profile.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.DisplayName.Should().Be("Updated Display Name");
|
||||
fetched.Thresholds.Should().Contain("8.0");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateVersion_CreatesNewVersion()
|
||||
{
|
||||
// Arrange
|
||||
var original = CreateProfile("version-create");
|
||||
original = await _repository.CreateAsync(original);
|
||||
|
||||
// Act
|
||||
var newVersion = new RiskProfileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "version-create",
|
||||
DisplayName = "New Version",
|
||||
Version = 2,
|
||||
IsActive = true
|
||||
};
|
||||
var created = await _repository.CreateVersionAsync(_tenantId, "version-create", newVersion);
|
||||
|
||||
// Assert
|
||||
created.Should().NotBeNull();
|
||||
created.Version.Should().Be(2);
|
||||
var originalAfter = await _repository.GetByIdAsync(_tenantId, original.Id);
|
||||
originalAfter!.IsActive.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Activate_SetsProfileAsActive()
|
||||
{
|
||||
// Arrange
|
||||
var profile = new RiskProfileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "activate-test",
|
||||
IsActive = false
|
||||
};
|
||||
await _repository.CreateAsync(profile);
|
||||
|
||||
// Act
|
||||
var result = await _repository.ActivateAsync(_tenantId, profile.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, profile.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.IsActive.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Deactivate_SetsProfileAsInactive()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateProfile("deactivate-test");
|
||||
await _repository.CreateAsync(profile);
|
||||
|
||||
// Act
|
||||
var result = await _repository.DeactivateAsync(_tenantId, profile.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, profile.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched!.IsActive.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Delete_RemovesProfile()
|
||||
{
|
||||
// Arrange
|
||||
var profile = CreateProfile("delete-test");
|
||||
await _repository.CreateAsync(profile);
|
||||
|
||||
// Act
|
||||
var result = await _repository.DeleteAsync(_tenantId, profile.Id);
|
||||
var fetched = await _repository.GetByIdAsync(_tenantId, profile.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
fetched.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateVersion_HistoryRemainsQueryableAndOrdered()
|
||||
{
|
||||
// Arrange
|
||||
var v1 = await _repository.CreateAsync(CreateProfile(
|
||||
name: "history-profile",
|
||||
thresholds: "{\"critical\":9.0}",
|
||||
scoringWeights: "{\"vulnerability\":1.0}"));
|
||||
|
||||
var v2 = new RiskProfileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "history-profile",
|
||||
DisplayName = "History V2",
|
||||
Description = "Second revision with tuned thresholds",
|
||||
Thresholds = "{\"critical\":8.0,\"high\":6.5}",
|
||||
ScoringWeights = "{\"vulnerability\":0.9}",
|
||||
Exemptions = "[]",
|
||||
Metadata = "{\"source\":\"unit-test\"}"
|
||||
};
|
||||
|
||||
// Act
|
||||
var createdV2 = await _repository.CreateVersionAsync(_tenantId, "history-profile", v2);
|
||||
|
||||
// Assert
|
||||
createdV2.Version.Should().Be(2);
|
||||
createdV2.IsActive.Should().BeTrue();
|
||||
|
||||
var versions = await _repository.GetVersionsByNameAsync(_tenantId, "history-profile");
|
||||
versions.Select(x => x.Version).Should().ContainInOrder(new[] { 2, 1 });
|
||||
versions.Single(x => x.Version == 1).IsActive.Should().BeFalse();
|
||||
versions.Single(x => x.Version == 1).Thresholds.Should().Contain("9.0");
|
||||
|
||||
var active = await _repository.GetActiveByNameAsync(_tenantId, "history-profile");
|
||||
active!.Version.Should().Be(2);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Activate_RevertsToPriorVersionAndDeactivatesCurrent()
|
||||
{
|
||||
// Arrange
|
||||
var v1 = await _repository.CreateAsync(CreateProfile("toggle-profile"));
|
||||
var v2 = await _repository.CreateVersionAsync(_tenantId, "toggle-profile", new RiskProfileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = "toggle-profile",
|
||||
DisplayName = "Toggle V2",
|
||||
Thresholds = "{\"critical\":8.5}"
|
||||
});
|
||||
|
||||
// Act
|
||||
var activated = await _repository.ActivateAsync(_tenantId, v1.Id);
|
||||
|
||||
// Assert
|
||||
activated.Should().BeTrue();
|
||||
var versions = await _repository.GetVersionsByNameAsync(_tenantId, "toggle-profile");
|
||||
versions.Single(x => x.Id == v1.Id).IsActive.Should().BeTrue();
|
||||
versions.Single(x => x.Id == v2.Id).IsActive.Should().BeFalse();
|
||||
|
||||
var active = await _repository.GetActiveByNameAsync(_tenantId, "toggle-profile");
|
||||
active!.Id.Should().Be(v1.Id);
|
||||
}
|
||||
|
||||
private RiskProfileEntity CreateProfile(
|
||||
string name,
|
||||
int version = 1,
|
||||
bool isActive = true,
|
||||
string? thresholds = null,
|
||||
string? scoringWeights = null) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = _tenantId,
|
||||
Name = name,
|
||||
Version = version,
|
||||
IsActive = isActive,
|
||||
Thresholds = thresholds ?? "{}",
|
||||
ScoringWeights = scoringWeights ?? "{}"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user