up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-29 11:08:08 +02:00
parent 7e7be4d2fd
commit 3488b22c0c
102 changed files with 18487 additions and 969 deletions

View File

@@ -0,0 +1,250 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Storage.Postgres.Models;
using StellaOps.Policy.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Policy.Storage.Postgres.Tests;
[Collection(PolicyPostgresCollection.Name)]
public sealed class EvaluationRunRepositoryTests : IAsyncLifetime
{
private readonly PolicyPostgresFixture _fixture;
private readonly EvaluationRunRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public EvaluationRunRepositoryTests(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 EvaluationRunRepository(dataSource, NullLogger<EvaluationRunRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CreateAndGetById_RoundTripsEvaluationRun()
{
// Arrange
var run = new EvaluationRunEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ProjectId = "project-123",
ArtifactId = "registry.example.com/app:v1.0",
PackId = Guid.NewGuid(),
PackVersion = 1,
Status = EvaluationStatus.Pending
};
// Act
await _repository.CreateAsync(run);
var fetched = await _repository.GetByIdAsync(_tenantId, run.Id);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(run.Id);
fetched.ProjectId.Should().Be("project-123");
fetched.Status.Should().Be(EvaluationStatus.Pending);
}
[Fact]
public async Task GetByProjectId_ReturnsProjectEvaluations()
{
// Arrange
var run = CreateRun("project-abc");
await _repository.CreateAsync(run);
// Act
var runs = await _repository.GetByProjectIdAsync(_tenantId, "project-abc");
// Assert
runs.Should().HaveCount(1);
runs[0].ProjectId.Should().Be("project-abc");
}
[Fact]
public async Task GetByArtifactId_ReturnsArtifactEvaluations()
{
// Arrange
var artifactId = "registry.example.com/app:v2.0";
var run = new EvaluationRunEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ArtifactId = artifactId,
Status = EvaluationStatus.Pending
};
await _repository.CreateAsync(run);
// Act
var runs = await _repository.GetByArtifactIdAsync(_tenantId, artifactId);
// Assert
runs.Should().HaveCount(1);
runs[0].ArtifactId.Should().Be(artifactId);
}
[Fact]
public async Task GetByStatus_ReturnsRunsWithStatus()
{
// Arrange
var pendingRun = CreateRun("project-1");
var completedRun = new EvaluationRunEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ProjectId = "project-2",
Status = EvaluationStatus.Completed,
Result = EvaluationResult.Pass
};
await _repository.CreateAsync(pendingRun);
await _repository.CreateAsync(completedRun);
// Act
var pendingRuns = await _repository.GetByStatusAsync(_tenantId, EvaluationStatus.Pending);
// Assert
pendingRuns.Should().HaveCount(1);
pendingRuns[0].ProjectId.Should().Be("project-1");
}
[Fact]
public async Task GetRecent_ReturnsRecentEvaluations()
{
// Arrange
await _repository.CreateAsync(CreateRun("project-1"));
await _repository.CreateAsync(CreateRun("project-2"));
// Act
var recentRuns = await _repository.GetRecentAsync(_tenantId, limit: 10);
// Assert
recentRuns.Should().HaveCount(2);
}
[Fact]
public async Task MarkStarted_UpdatesStatusAndStartedAt()
{
// Arrange
var run = CreateRun("project-start");
await _repository.CreateAsync(run);
// Act
var result = await _repository.MarkStartedAsync(_tenantId, run.Id);
var fetched = await _repository.GetByIdAsync(_tenantId, run.Id);
// Assert
result.Should().BeTrue();
fetched!.Status.Should().Be(EvaluationStatus.Running);
fetched.StartedAt.Should().NotBeNull();
}
[Fact]
public async Task MarkCompleted_UpdatesAllCompletionFields()
{
// Arrange
var run = CreateRun("project-complete");
await _repository.CreateAsync(run);
await _repository.MarkStartedAsync(_tenantId, run.Id);
// Act
var result = await _repository.MarkCompletedAsync(
_tenantId,
run.Id,
EvaluationResult.Fail,
score: 65.5m,
findingsCount: 10,
criticalCount: 2,
highCount: 3,
mediumCount: 4,
lowCount: 1,
durationMs: 1500);
var fetched = await _repository.GetByIdAsync(_tenantId, run.Id);
// Assert
result.Should().BeTrue();
fetched!.Status.Should().Be(EvaluationStatus.Completed);
fetched.Result.Should().Be(EvaluationResult.Fail);
fetched.Score.Should().Be(65.5m);
fetched.FindingsCount.Should().Be(10);
fetched.CriticalCount.Should().Be(2);
fetched.DurationMs.Should().Be(1500);
fetched.CompletedAt.Should().NotBeNull();
}
[Fact]
public async Task MarkFailed_SetsErrorMessage()
{
// Arrange
var run = CreateRun("project-fail");
await _repository.CreateAsync(run);
// Act
var result = await _repository.MarkFailedAsync(_tenantId, run.Id, "Policy engine timeout");
var fetched = await _repository.GetByIdAsync(_tenantId, run.Id);
// Assert
result.Should().BeTrue();
fetched!.Status.Should().Be(EvaluationStatus.Failed);
fetched.Result.Should().Be(EvaluationResult.Error);
fetched.ErrorMessage.Should().Be("Policy engine timeout");
}
[Fact]
public async Task GetStats_ReturnsCorrectStatistics()
{
// Arrange
var passedRun = new EvaluationRunEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Status = EvaluationStatus.Completed,
Result = EvaluationResult.Pass,
Score = 100,
FindingsCount = 0,
CriticalCount = 0,
HighCount = 0
};
var failedRun = new EvaluationRunEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Status = EvaluationStatus.Completed,
Result = EvaluationResult.Fail,
Score = 50,
FindingsCount = 5,
CriticalCount = 1,
HighCount = 2
};
await _repository.CreateAsync(passedRun);
await _repository.CreateAsync(failedRun);
var from = DateTimeOffset.UtcNow.AddHours(-1);
var to = DateTimeOffset.UtcNow.AddHours(1);
// Act
var stats = await _repository.GetStatsAsync(_tenantId, from, to);
// Assert
stats.Total.Should().Be(2);
stats.Passed.Should().Be(1);
stats.Failed.Should().Be(1);
stats.TotalFindings.Should().Be(5);
stats.CriticalFindings.Should().Be(1);
stats.HighFindings.Should().Be(2);
}
private EvaluationRunEntity CreateRun(string projectId) => new()
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
ProjectId = projectId,
Status = EvaluationStatus.Pending
};
}

View File

@@ -0,0 +1,278 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Storage.Postgres.Models;
using StellaOps.Policy.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Policy.Storage.Postgres.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;
[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);
}
[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);
}
[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"]);
}
[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");
}
[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");
}
[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");
}
[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");
}
[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();
}
[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();
}
[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);
}
[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
};
}

View File

@@ -0,0 +1,213 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Storage.Postgres.Models;
using StellaOps.Policy.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Policy.Storage.Postgres.Tests;
[Collection(PolicyPostgresCollection.Name)]
public sealed class PackRepositoryTests : IAsyncLifetime
{
private readonly PolicyPostgresFixture _fixture;
private readonly PackRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public PackRepositoryTests(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 PackRepository(dataSource, NullLogger<PackRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CreateAndGetById_RoundTripsPack()
{
// Arrange
var pack = new PackEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "security-baseline",
DisplayName = "Security Baseline Pack",
Description = "Core security policy rules",
IsBuiltin = false
};
// Act
await _repository.CreateAsync(pack);
var fetched = await _repository.GetByIdAsync(_tenantId, pack.Id);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(pack.Id);
fetched.Name.Should().Be("security-baseline");
fetched.DisplayName.Should().Be("Security Baseline Pack");
}
[Fact]
public async Task GetByName_ReturnsCorrectPack()
{
// Arrange
var pack = CreatePack("compliance-pack");
await _repository.CreateAsync(pack);
// Act
var fetched = await _repository.GetByNameAsync(_tenantId, "compliance-pack");
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(pack.Id);
}
[Fact]
public async Task GetAll_ReturnsAllPacksForTenant()
{
// Arrange
var pack1 = CreatePack("pack1");
var pack2 = CreatePack("pack2");
await _repository.CreateAsync(pack1);
await _repository.CreateAsync(pack2);
// Act
var packs = await _repository.GetAllAsync(_tenantId);
// Assert
packs.Should().HaveCount(2);
packs.Select(p => p.Name).Should().Contain(["pack1", "pack2"]);
}
[Fact]
public async Task GetAll_ExcludesDeprecated()
{
// Arrange
var activePack = CreatePack("active");
var deprecatedPack = new PackEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "deprecated",
IsDeprecated = true
};
await _repository.CreateAsync(activePack);
await _repository.CreateAsync(deprecatedPack);
// Act
var packs = await _repository.GetAllAsync(_tenantId, includeDeprecated: false);
// Assert
packs.Should().HaveCount(1);
packs[0].Name.Should().Be("active");
}
[Fact]
public async Task GetBuiltin_ReturnsOnlyBuiltinPacks()
{
// Arrange
var builtinPack = new PackEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "builtin",
IsBuiltin = true
};
var customPack = CreatePack("custom");
await _repository.CreateAsync(builtinPack);
await _repository.CreateAsync(customPack);
// Act
var builtinPacks = await _repository.GetBuiltinAsync(_tenantId);
// Assert
builtinPacks.Should().HaveCount(1);
builtinPacks[0].Name.Should().Be("builtin");
}
[Fact]
public async Task Update_ModifiesPack()
{
// Arrange
var pack = CreatePack("update-test");
await _repository.CreateAsync(pack);
// Act
var updated = new PackEntity
{
Id = pack.Id,
TenantId = _tenantId,
Name = "update-test",
DisplayName = "Updated Display Name",
Description = "Updated description"
};
var result = await _repository.UpdateAsync(updated);
var fetched = await _repository.GetByIdAsync(_tenantId, pack.Id);
// Assert
result.Should().BeTrue();
fetched!.DisplayName.Should().Be("Updated Display Name");
fetched.Description.Should().Be("Updated description");
}
[Fact]
public async Task SetActiveVersion_UpdatesActiveVersion()
{
// Arrange
var pack = CreatePack("version-test");
await _repository.CreateAsync(pack);
// Act
var result = await _repository.SetActiveVersionAsync(_tenantId, pack.Id, 2);
var fetched = await _repository.GetByIdAsync(_tenantId, pack.Id);
// Assert
result.Should().BeTrue();
fetched!.ActiveVersion.Should().Be(2);
}
[Fact]
public async Task Deprecate_MarksParkAsDeprecated()
{
// Arrange
var pack = CreatePack("deprecate-test");
await _repository.CreateAsync(pack);
// Act
var result = await _repository.DeprecateAsync(_tenantId, pack.Id);
var fetched = await _repository.GetByIdAsync(_tenantId, pack.Id);
// Assert
result.Should().BeTrue();
fetched!.IsDeprecated.Should().BeTrue();
}
[Fact]
public async Task Delete_RemovesPack()
{
// Arrange
var pack = CreatePack("delete-test");
await _repository.CreateAsync(pack);
// Act
var result = await _repository.DeleteAsync(_tenantId, pack.Id);
var fetched = await _repository.GetByIdAsync(_tenantId, pack.Id);
// Assert
result.Should().BeTrue();
fetched.Should().BeNull();
}
private PackEntity CreatePack(string name) => new()
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = name,
IsBuiltin = false
};
}

View File

@@ -0,0 +1,191 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Storage.Postgres.Models;
using StellaOps.Policy.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Policy.Storage.Postgres.Tests;
[Collection(PolicyPostgresCollection.Name)]
public sealed class PolicyAuditRepositoryTests : IAsyncLifetime
{
private readonly PolicyPostgresFixture _fixture;
private readonly PolicyAuditRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public PolicyAuditRepositoryTests(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 PolicyAuditRepository(dataSource, NullLogger<PolicyAuditRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task Create_ReturnsGeneratedId()
{
// Arrange
var audit = new PolicyAuditEntity
{
TenantId = _tenantId,
UserId = Guid.NewGuid(),
Action = "pack.created",
ResourceType = "pack",
ResourceId = Guid.NewGuid().ToString()
};
// Act
var id = await _repository.CreateAsync(audit);
// Assert
id.Should().BeGreaterThan(0);
}
[Fact]
public async Task List_ReturnsAuditEntriesOrderedByCreatedAtDesc()
{
// Arrange
var audit1 = CreateAudit("action1");
var audit2 = CreateAudit("action2");
await _repository.CreateAsync(audit1);
await Task.Delay(10);
await _repository.CreateAsync(audit2);
// Act
var audits = await _repository.ListAsync(_tenantId, limit: 10);
// Assert
audits.Should().HaveCount(2);
audits[0].Action.Should().Be("action2"); // Most recent first
}
[Fact]
public async Task GetByResource_ReturnsResourceAudits()
{
// Arrange
var resourceId = Guid.NewGuid().ToString();
var audit = new PolicyAuditEntity
{
TenantId = _tenantId,
Action = "exception.updated",
ResourceType = "exception",
ResourceId = resourceId
};
await _repository.CreateAsync(audit);
// Act
var audits = await _repository.GetByResourceAsync(_tenantId, "exception", resourceId);
// Assert
audits.Should().HaveCount(1);
audits[0].ResourceId.Should().Be(resourceId);
}
[Fact]
public async Task GetByResource_WithoutResourceId_ReturnsAllOfType()
{
// Arrange
await _repository.CreateAsync(new PolicyAuditEntity
{
TenantId = _tenantId,
Action = "pack.created",
ResourceType = "pack",
ResourceId = Guid.NewGuid().ToString()
});
await _repository.CreateAsync(new PolicyAuditEntity
{
TenantId = _tenantId,
Action = "pack.updated",
ResourceType = "pack",
ResourceId = Guid.NewGuid().ToString()
});
// Act
var audits = await _repository.GetByResourceAsync(_tenantId, "pack");
// Assert
audits.Should().HaveCount(2);
}
[Fact]
public async Task GetByCorrelationId_ReturnsCorrelatedAudits()
{
// Arrange
var correlationId = Guid.NewGuid().ToString();
var audit1 = new PolicyAuditEntity
{
TenantId = _tenantId,
Action = "evaluation.started",
ResourceType = "evaluation",
CorrelationId = correlationId
};
var audit2 = new PolicyAuditEntity
{
TenantId = _tenantId,
Action = "evaluation.completed",
ResourceType = "evaluation",
CorrelationId = correlationId
};
await _repository.CreateAsync(audit1);
await _repository.CreateAsync(audit2);
// Act
var audits = await _repository.GetByCorrelationIdAsync(_tenantId, correlationId);
// Assert
audits.Should().HaveCount(2);
audits.Should().AllSatisfy(a => a.CorrelationId.Should().Be(correlationId));
}
[Fact]
public async Task Create_StoresJsonbValues()
{
// Arrange
var audit = new PolicyAuditEntity
{
TenantId = _tenantId,
Action = "profile.updated",
ResourceType = "risk_profile",
OldValue = "{\"threshold\": 7.0}",
NewValue = "{\"threshold\": 8.0}"
};
// Act
await _repository.CreateAsync(audit);
var audits = await _repository.GetByResourceAsync(_tenantId, "risk_profile");
// Assert
audits.Should().HaveCount(1);
audits[0].OldValue.Should().Contain("7.0");
audits[0].NewValue.Should().Contain("8.0");
}
[Fact]
public async Task DeleteOld_RemovesOldAudits()
{
// Arrange
await _repository.CreateAsync(CreateAudit("old-action"));
// Act - Delete audits older than future date
var cutoff = DateTimeOffset.UtcNow.AddMinutes(1);
var count = await _repository.DeleteOldAsync(cutoff);
// Assert
count.Should().Be(1);
}
private PolicyAuditEntity CreateAudit(string action) => new()
{
TenantId = _tenantId,
UserId = Guid.NewGuid(),
Action = action,
ResourceType = "test",
ResourceId = Guid.NewGuid().ToString()
};
}

View File

@@ -0,0 +1,274 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Storage.Postgres.Models;
using StellaOps.Policy.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Policy.Storage.Postgres.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;
[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();
}
[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();
}
[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"]);
}
[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");
}
[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]);
}
[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");
}
[Fact]
public async Task CreateVersion_CreatesNewVersion()
{
// Arrange
var original = CreateProfile("version-create");
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);
}
[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();
}
[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();
}
[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();
}
private RiskProfileEntity CreateProfile(string name) => new()
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = name,
Version = 1,
IsActive = true
};
}

View File

@@ -0,0 +1,231 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Storage.Postgres.Models;
using StellaOps.Policy.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Policy.Storage.Postgres.Tests;
[Collection(PolicyPostgresCollection.Name)]
public sealed class RuleRepositoryTests : IAsyncLifetime
{
private readonly PolicyPostgresFixture _fixture;
private readonly RuleRepository _repository;
private readonly Guid _packVersionId = Guid.NewGuid();
public RuleRepositoryTests(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 RuleRepository(dataSource, NullLogger<RuleRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CreateAndGetById_RoundTripsRule()
{
// Arrange
var rule = new RuleEntity
{
Id = Guid.NewGuid(),
PackVersionId = _packVersionId,
Name = "no-root-containers",
Description = "Containers should not run as root",
RuleType = RuleType.Rego,
Content = "package container\ndefault allow = false",
ContentHash = "abc123",
Severity = RuleSeverity.High,
Category = "security",
Tags = ["container", "security"]
};
// Act
await _repository.CreateAsync(rule);
var fetched = await _repository.GetByIdAsync(rule.Id);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(rule.Id);
fetched.Name.Should().Be("no-root-containers");
fetched.Severity.Should().Be(RuleSeverity.High);
}
[Fact]
public async Task GetByName_ReturnsCorrectRule()
{
// Arrange
var rule = CreateRule("required-labels");
await _repository.CreateAsync(rule);
// Act
var fetched = await _repository.GetByNameAsync(_packVersionId, "required-labels");
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(rule.Id);
}
[Fact]
public async Task CreateBatch_CreatesMultipleRules()
{
// Arrange
var rules = new[]
{
CreateRule("rule1"),
CreateRule("rule2"),
CreateRule("rule3")
};
// Act
var count = await _repository.CreateBatchAsync(rules);
// Assert
count.Should().Be(3);
}
[Fact]
public async Task GetByPackVersionId_ReturnsAllRulesForVersion()
{
// Arrange
var rule1 = CreateRule("rule1");
var rule2 = CreateRule("rule2");
await _repository.CreateAsync(rule1);
await _repository.CreateAsync(rule2);
// Act
var rules = await _repository.GetByPackVersionIdAsync(_packVersionId);
// Assert
rules.Should().HaveCount(2);
rules.Select(r => r.Name).Should().Contain(["rule1", "rule2"]);
}
[Fact]
public async Task GetBySeverity_ReturnsRulesWithSeverity()
{
// Arrange
var criticalRule = new RuleEntity
{
Id = Guid.NewGuid(),
PackVersionId = _packVersionId,
Name = "critical-rule",
Content = "content",
ContentHash = "hash",
Severity = RuleSeverity.Critical
};
var lowRule = new RuleEntity
{
Id = Guid.NewGuid(),
PackVersionId = _packVersionId,
Name = "low-rule",
Content = "content",
ContentHash = "hash2",
Severity = RuleSeverity.Low
};
await _repository.CreateAsync(criticalRule);
await _repository.CreateAsync(lowRule);
// Act
var criticalRules = await _repository.GetBySeverityAsync(_packVersionId, RuleSeverity.Critical);
// Assert
criticalRules.Should().HaveCount(1);
criticalRules[0].Name.Should().Be("critical-rule");
}
[Fact]
public async Task GetByCategory_ReturnsRulesInCategory()
{
// Arrange
var securityRule = new RuleEntity
{
Id = Guid.NewGuid(),
PackVersionId = _packVersionId,
Name = "security-rule",
Content = "content",
ContentHash = "hash",
Category = "security"
};
var complianceRule = new RuleEntity
{
Id = Guid.NewGuid(),
PackVersionId = _packVersionId,
Name = "compliance-rule",
Content = "content",
ContentHash = "hash2",
Category = "compliance"
};
await _repository.CreateAsync(securityRule);
await _repository.CreateAsync(complianceRule);
// Act
var securityRules = await _repository.GetByCategoryAsync(_packVersionId, "security");
// Assert
securityRules.Should().HaveCount(1);
securityRules[0].Name.Should().Be("security-rule");
}
[Fact]
public async Task GetByTag_ReturnsRulesWithTag()
{
// Arrange
var containerRule = new RuleEntity
{
Id = Guid.NewGuid(),
PackVersionId = _packVersionId,
Name = "container-rule",
Content = "content",
ContentHash = "hash",
Tags = ["container", "docker"]
};
var networkRule = new RuleEntity
{
Id = Guid.NewGuid(),
PackVersionId = _packVersionId,
Name = "network-rule",
Content = "content",
ContentHash = "hash2",
Tags = ["network"]
};
await _repository.CreateAsync(containerRule);
await _repository.CreateAsync(networkRule);
// Act
var containerRules = await _repository.GetByTagAsync(_packVersionId, "container");
// Assert
containerRules.Should().HaveCount(1);
containerRules[0].Name.Should().Be("container-rule");
}
[Fact]
public async Task CountByPackVersionId_ReturnsCorrectCount()
{
// Arrange
await _repository.CreateAsync(CreateRule("rule1"));
await _repository.CreateAsync(CreateRule("rule2"));
await _repository.CreateAsync(CreateRule("rule3"));
// Act
var count = await _repository.CountByPackVersionIdAsync(_packVersionId);
// Assert
count.Should().Be(3);
}
private RuleEntity CreateRule(string name) => new()
{
Id = Guid.NewGuid(),
PackVersionId = _packVersionId,
Name = name,
Content = "package test",
ContentHash = Guid.NewGuid().ToString()
};
}