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.Instance); _repository = new RiskProfileRepository(dataSource, NullLogger.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 ?? "{}" }; }