up
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
namespace StellaOps.Policy.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Entity representing an audit log entry for the policy module.
|
||||
/// </summary>
|
||||
public sealed class PolicyAuditEntity
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public Guid? UserId { get; init; }
|
||||
public required string Action { get; init; }
|
||||
public required string ResourceType { get; init; }
|
||||
public string? ResourceId { get; init; }
|
||||
public string? OldValue { get; init; }
|
||||
public string? NewValue { get; init; }
|
||||
public string? CorrelationId { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for explanation operations.
|
||||
/// </summary>
|
||||
public sealed class ExplanationRepository : RepositoryBase<PolicyDataSource>, IExplanationRepository
|
||||
{
|
||||
public ExplanationRepository(PolicyDataSource dataSource, ILogger<ExplanationRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<ExplanationEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, evaluation_run_id, rule_id, rule_name, result, severity, message, details, remediation, resource_path, line_number, created_at
|
||||
FROM policy.explanations WHERE id = @id
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await reader.ReadAsync(cancellationToken).ConfigureAwait(false) ? MapExplanation(reader) : null;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ExplanationEntity>> GetByEvaluationRunIdAsync(Guid evaluationRunId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, evaluation_run_id, rule_id, rule_name, result, severity, message, details, remediation, resource_path, line_number, created_at
|
||||
FROM policy.explanations WHERE evaluation_run_id = @evaluation_run_id
|
||||
ORDER BY created_at
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "evaluation_run_id", evaluationRunId);
|
||||
var results = new List<ExplanationEntity>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
results.Add(MapExplanation(reader));
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ExplanationEntity>> GetByEvaluationRunIdAndResultAsync(Guid evaluationRunId, RuleResult result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, evaluation_run_id, rule_id, rule_name, result, severity, message, details, remediation, resource_path, line_number, created_at
|
||||
FROM policy.explanations WHERE evaluation_run_id = @evaluation_run_id AND result = @result
|
||||
ORDER BY severity DESC, created_at
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "evaluation_run_id", evaluationRunId);
|
||||
AddParameter(command, "result", ResultToString(result));
|
||||
var results = new List<ExplanationEntity>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
results.Add(MapExplanation(reader));
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<ExplanationEntity> CreateAsync(ExplanationEntity explanation, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO policy.explanations (id, evaluation_run_id, rule_id, rule_name, result, severity, message, details, remediation, resource_path, line_number)
|
||||
VALUES (@id, @evaluation_run_id, @rule_id, @rule_name, @result, @severity, @message, @details::jsonb, @remediation, @resource_path, @line_number)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = explanation.Id == Guid.Empty ? Guid.NewGuid() : explanation.Id;
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "evaluation_run_id", explanation.EvaluationRunId);
|
||||
AddParameter(command, "rule_id", explanation.RuleId);
|
||||
AddParameter(command, "rule_name", explanation.RuleName);
|
||||
AddParameter(command, "result", ResultToString(explanation.Result));
|
||||
AddParameter(command, "severity", explanation.Severity);
|
||||
AddParameter(command, "message", explanation.Message);
|
||||
AddJsonbParameter(command, "details", explanation.Details);
|
||||
AddParameter(command, "remediation", explanation.Remediation);
|
||||
AddParameter(command, "resource_path", explanation.ResourcePath);
|
||||
AddParameter(command, "line_number", explanation.LineNumber);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapExplanation(reader);
|
||||
}
|
||||
|
||||
public async Task<int> CreateBatchAsync(IEnumerable<ExplanationEntity> explanations, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO policy.explanations (id, evaluation_run_id, rule_id, rule_name, result, severity, message, details, remediation, resource_path, line_number)
|
||||
VALUES (@id, @evaluation_run_id, @rule_id, @rule_name, @result, @severity, @message, @details::jsonb, @remediation, @resource_path, @line_number)
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
var count = 0;
|
||||
foreach (var explanation in explanations)
|
||||
{
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
var id = explanation.Id == Guid.Empty ? Guid.NewGuid() : explanation.Id;
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "evaluation_run_id", explanation.EvaluationRunId);
|
||||
AddParameter(command, "rule_id", explanation.RuleId);
|
||||
AddParameter(command, "rule_name", explanation.RuleName);
|
||||
AddParameter(command, "result", ResultToString(explanation.Result));
|
||||
AddParameter(command, "severity", explanation.Severity);
|
||||
AddParameter(command, "message", explanation.Message);
|
||||
AddJsonbParameter(command, "details", explanation.Details);
|
||||
AddParameter(command, "remediation", explanation.Remediation);
|
||||
AddParameter(command, "resource_path", explanation.ResourcePath);
|
||||
AddParameter(command, "line_number", explanation.LineNumber);
|
||||
count += await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteByEvaluationRunIdAsync(Guid evaluationRunId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM policy.explanations WHERE evaluation_run_id = @evaluation_run_id";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "evaluation_run_id", evaluationRunId);
|
||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
private static ExplanationEntity MapExplanation(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
EvaluationRunId = reader.GetGuid(1),
|
||||
RuleId = GetNullableGuid(reader, 2),
|
||||
RuleName = reader.GetString(3),
|
||||
Result = ParseResult(reader.GetString(4)),
|
||||
Severity = reader.GetString(5),
|
||||
Message = GetNullableString(reader, 6),
|
||||
Details = reader.GetString(7),
|
||||
Remediation = GetNullableString(reader, 8),
|
||||
ResourcePath = GetNullableString(reader, 9),
|
||||
LineNumber = reader.IsDBNull(10) ? null : reader.GetInt32(10),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(11)
|
||||
};
|
||||
|
||||
private static string ResultToString(RuleResult result) => result switch
|
||||
{
|
||||
RuleResult.Pass => "pass",
|
||||
RuleResult.Fail => "fail",
|
||||
RuleResult.Skip => "skip",
|
||||
RuleResult.Error => "error",
|
||||
_ => throw new ArgumentException($"Unknown result: {result}")
|
||||
};
|
||||
|
||||
private static RuleResult ParseResult(string result) => result switch
|
||||
{
|
||||
"pass" => RuleResult.Pass,
|
||||
"fail" => RuleResult.Fail,
|
||||
"skip" => RuleResult.Skip,
|
||||
"error" => RuleResult.Error,
|
||||
_ => throw new ArgumentException($"Unknown result: {result}")
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for explanation operations.
|
||||
/// </summary>
|
||||
public interface IExplanationRepository
|
||||
{
|
||||
Task<ExplanationEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<ExplanationEntity>> GetByEvaluationRunIdAsync(Guid evaluationRunId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<ExplanationEntity>> GetByEvaluationRunIdAndResultAsync(Guid evaluationRunId, RuleResult result, CancellationToken cancellationToken = default);
|
||||
Task<ExplanationEntity> CreateAsync(ExplanationEntity explanation, CancellationToken cancellationToken = default);
|
||||
Task<int> CreateBatchAsync(IEnumerable<ExplanationEntity> explanations, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteByEvaluationRunIdAsync(Guid evaluationRunId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for policy audit operations.
|
||||
/// </summary>
|
||||
public interface IPolicyAuditRepository
|
||||
{
|
||||
Task<long> CreateAsync(PolicyAuditEntity audit, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<PolicyAuditEntity>> ListAsync(string tenantId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<PolicyAuditEntity>> GetByResourceAsync(string tenantId, string resourceType, string? resourceId = null, int limit = 100, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<PolicyAuditEntity>> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteOldAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for policy audit operations.
|
||||
/// </summary>
|
||||
public sealed class PolicyAuditRepository : RepositoryBase<PolicyDataSource>, IPolicyAuditRepository
|
||||
{
|
||||
public PolicyAuditRepository(PolicyDataSource dataSource, ILogger<PolicyAuditRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<long> CreateAsync(PolicyAuditEntity audit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO policy.audit (tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, correlation_id)
|
||||
VALUES (@tenant_id, @user_id, @action, @resource_type, @resource_id, @old_value::jsonb, @new_value::jsonb, @correlation_id)
|
||||
RETURNING id
|
||||
""";
|
||||
await using var connection = await DataSource.OpenConnectionAsync(audit.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "tenant_id", audit.TenantId);
|
||||
AddParameter(command, "user_id", audit.UserId);
|
||||
AddParameter(command, "action", audit.Action);
|
||||
AddParameter(command, "resource_type", audit.ResourceType);
|
||||
AddParameter(command, "resource_id", audit.ResourceId);
|
||||
AddJsonbParameter(command, "old_value", audit.OldValue);
|
||||
AddJsonbParameter(command, "new_value", audit.NewValue);
|
||||
AddParameter(command, "correlation_id", audit.CorrelationId);
|
||||
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
return (long)result!;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PolicyAuditEntity>> ListAsync(string tenantId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, correlation_id, created_at
|
||||
FROM policy.audit WHERE tenant_id = @tenant_id
|
||||
ORDER BY created_at DESC LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
}, MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PolicyAuditEntity>> GetByResourceAsync(string tenantId, string resourceType, string? resourceId = null, int limit = 100, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, correlation_id, created_at
|
||||
FROM policy.audit WHERE tenant_id = @tenant_id AND resource_type = @resource_type
|
||||
""";
|
||||
if (resourceId != null) sql += " AND resource_id = @resource_id";
|
||||
sql += " ORDER BY created_at DESC LIMIT @limit";
|
||||
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "resource_type", resourceType);
|
||||
if (resourceId != null) AddParameter(cmd, "resource_id", resourceId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
}, MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PolicyAuditEntity>> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, correlation_id, created_at
|
||||
FROM policy.audit WHERE tenant_id = @tenant_id AND correlation_id = @correlation_id
|
||||
ORDER BY created_at
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "correlation_id", correlationId); },
|
||||
MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> DeleteOldAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM policy.audit WHERE created_at < @cutoff";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "cutoff", cutoff);
|
||||
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static PolicyAuditEntity MapAudit(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetInt64(0),
|
||||
TenantId = reader.GetString(1),
|
||||
UserId = GetNullableGuid(reader, 2),
|
||||
Action = reader.GetString(3),
|
||||
ResourceType = reader.GetString(4),
|
||||
ResourceId = GetNullableString(reader, 5),
|
||||
OldValue = GetNullableString(reader, 6),
|
||||
NewValue = GetNullableString(reader, 7),
|
||||
CorrelationId = GetNullableString(reader, 8),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(9)
|
||||
};
|
||||
}
|
||||
@@ -35,6 +35,8 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IEvaluationRunRepository, EvaluationRunRepository>();
|
||||
services.AddScoped<IExceptionRepository, ExceptionRepository>();
|
||||
services.AddScoped<IReceiptRepository, PostgresReceiptRepository>();
|
||||
services.AddScoped<IExplanationRepository, ExplanationRepository>();
|
||||
services.AddScoped<IPolicyAuditRepository, PolicyAuditRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -60,6 +62,8 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IEvaluationRunRepository, EvaluationRunRepository>();
|
||||
services.AddScoped<IExceptionRepository, ExceptionRepository>();
|
||||
services.AddScoped<IReceiptRepository, PostgresReceiptRepository>();
|
||||
services.AddScoped<IExplanationRepository, ExplanationRepository>();
|
||||
services.AddScoped<IPolicyAuditRepository, PolicyAuditRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user