partly or unimplemented features - now implemented

This commit is contained in:
master
2026-02-09 08:53:51 +02:00
parent 1bf6bbf395
commit 4bdc298ec1
674 changed files with 90194 additions and 2271 deletions

View File

@@ -0,0 +1,521 @@
// -----------------------------------------------------------------------------
// SchemaIsolationServiceTests.cs
// Sprint: SPRINT_20260208_018_Attestor_postgresql_persistence_layer
// Task: T1 — Tests for schema isolation, RLS scaffolding, temporal table management
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Diagnostics.Metrics;
using FluentAssertions;
using Xunit;
namespace StellaOps.Attestor.Persistence.Tests;
internal sealed class TestSchemaIsolationMeterFactory : IMeterFactory
{
private readonly List<Meter> _meters = [];
public Meter Create(MeterOptions options)
{
var meter = new Meter(options);
_meters.Add(meter);
return meter;
}
public void Dispose()
{
foreach (var m in _meters) m.Dispose();
}
}
internal sealed class FakeSchemaTimeProvider : TimeProvider
{
private DateTimeOffset _utcNow = new(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
public override DateTimeOffset GetUtcNow() => _utcNow;
public void Advance(TimeSpan delta) => _utcNow = _utcNow.Add(delta);
}
public class SchemaIsolationServiceTests : IDisposable
{
private readonly TestSchemaIsolationMeterFactory _meterFactory = new();
private readonly FakeSchemaTimeProvider _timeProvider = new();
private readonly SchemaIsolationService _service;
public SchemaIsolationServiceTests()
{
_service = new SchemaIsolationService(_timeProvider, _meterFactory);
}
public void Dispose() => _meterFactory.Dispose();
// ---------------------------------------------------------------
// GetAssignment
// ---------------------------------------------------------------
[Theory]
[InlineData(AttestorSchema.ProofChain, "proofchain")]
[InlineData(AttestorSchema.Attestor, "attestor")]
[InlineData(AttestorSchema.Verdict, "verdict")]
[InlineData(AttestorSchema.Watchlist, "watchlist")]
[InlineData(AttestorSchema.Audit, "audit")]
public void GetAssignment_returns_correct_schema_name(AttestorSchema schema, string expectedName)
{
var assignment = _service.GetAssignment(schema);
assignment.SchemaName.Should().Be(expectedName);
assignment.Schema.Should().Be(schema);
}
[Fact]
public void GetAssignment_ProofChain_has_six_tables()
{
var assignment = _service.GetAssignment(AttestorSchema.ProofChain);
assignment.Tables.Should().HaveCount(6);
assignment.Tables.Should().Contain("sbom_entries");
assignment.Tables.Should().Contain("dsse_envelopes");
assignment.Tables.Should().Contain("spines");
assignment.Tables.Should().Contain("trust_anchors");
assignment.Tables.Should().Contain("rekor_entries");
assignment.Tables.Should().Contain("audit_log");
}
[Fact]
public void GetAssignment_Verdict_has_tables()
{
var assignment = _service.GetAssignment(AttestorSchema.Verdict);
assignment.Tables.Should().Contain("verdict_ledger");
assignment.Tables.Should().Contain("verdict_policies");
}
[Fact]
public void GetAssignment_invalid_value_throws()
{
var act = () => _service.GetAssignment((AttestorSchema)999);
act.Should().Throw<ArgumentException>();
}
// ---------------------------------------------------------------
// GetAllAssignments
// ---------------------------------------------------------------
[Fact]
public void GetAllAssignments_returns_all_five_schemas()
{
var all = _service.GetAllAssignments();
all.Should().HaveCount(5);
all.Select(a => a.Schema).Should().Contain(AttestorSchema.ProofChain);
all.Select(a => a.Schema).Should().Contain(AttestorSchema.Attestor);
all.Select(a => a.Schema).Should().Contain(AttestorSchema.Verdict);
all.Select(a => a.Schema).Should().Contain(AttestorSchema.Watchlist);
all.Select(a => a.Schema).Should().Contain(AttestorSchema.Audit);
}
[Fact]
public void GetAllAssignments_every_assignment_has_at_least_one_table()
{
var all = _service.GetAllAssignments();
foreach (var a in all)
{
a.Tables.Should().NotBeEmpty($"schema {a.Schema} should have at least one table");
}
}
// ---------------------------------------------------------------
// GenerateProvisioningSql
// ---------------------------------------------------------------
[Theory]
[InlineData(AttestorSchema.ProofChain, "proofchain")]
[InlineData(AttestorSchema.Attestor, "attestor")]
[InlineData(AttestorSchema.Verdict, "verdict")]
[InlineData(AttestorSchema.Watchlist, "watchlist")]
[InlineData(AttestorSchema.Audit, "audit")]
public void GenerateProvisioningSql_generates_create_schema(AttestorSchema schema, string schemaName)
{
var result = _service.GenerateProvisioningSql(schema);
result.Success.Should().BeTrue();
result.Schema.Should().Be(schema);
result.GeneratedStatements.Should().Contain(s => s.Contains($"CREATE SCHEMA IF NOT EXISTS {schemaName}"));
}
[Fact]
public void GenerateProvisioningSql_includes_grant_statement()
{
var result = _service.GenerateProvisioningSql(AttestorSchema.Verdict);
result.GeneratedStatements.Should().Contain(s => s.Contains("GRANT USAGE"));
result.GeneratedStatements.Should().Contain(s => s.Contains("stellaops_app"));
}
[Fact]
public void GenerateProvisioningSql_includes_default_privileges()
{
var result = _service.GenerateProvisioningSql(AttestorSchema.ProofChain);
result.GeneratedStatements.Should().Contain(s => s.Contains("ALTER DEFAULT PRIVILEGES"));
}
[Fact]
public void GenerateProvisioningSql_includes_comment()
{
var result = _service.GenerateProvisioningSql(AttestorSchema.Audit);
result.GeneratedStatements.Should().Contain(s => s.Contains("COMMENT ON SCHEMA"));
}
[Fact]
public void GenerateProvisioningSql_records_timestamp()
{
var result = _service.GenerateProvisioningSql(AttestorSchema.ProofChain);
result.Timestamp.Should().Be(_timeProvider.GetUtcNow());
}
[Fact]
public void GenerateProvisioningSql_produces_four_statements()
{
var result = _service.GenerateProvisioningSql(AttestorSchema.ProofChain);
result.GeneratedStatements.Should().HaveCount(4);
}
// ---------------------------------------------------------------
// GetRlsPolicies
// ---------------------------------------------------------------
[Fact]
public void GetRlsPolicies_Verdict_returns_policies()
{
var policies = _service.GetRlsPolicies(AttestorSchema.Verdict);
policies.Should().NotBeEmpty();
policies.Should().OnlyContain(p => p.Schema == AttestorSchema.Verdict);
}
[Fact]
public void GetRlsPolicies_ProofChain_returns_empty()
{
// ProofChain does not have tenant isolation (shared read-only data)
var policies = _service.GetRlsPolicies(AttestorSchema.ProofChain);
policies.Should().BeEmpty();
}
[Fact]
public void GetRlsPolicies_all_have_tenant_column()
{
foreach (var schema in Enum.GetValues<AttestorSchema>())
{
var policies = _service.GetRlsPolicies(schema);
policies.Should().OnlyContain(p => p.TenantColumn == "tenant_id");
}
}
[Fact]
public void RlsPolicyDefinition_UsingExpression_computed_correctly()
{
var policies = _service.GetRlsPolicies(AttestorSchema.Verdict);
var policy = policies.First();
policy.UsingExpression.Should().Contain("tenant_id");
policy.UsingExpression.Should().Contain("current_setting");
}
// ---------------------------------------------------------------
// GenerateRlsSql
// ---------------------------------------------------------------
[Fact]
public void GenerateRlsSql_Verdict_generates_enable_and_policy()
{
var result = _service.GenerateRlsSql(AttestorSchema.Verdict);
result.Success.Should().BeTrue();
result.GeneratedStatements.Should().Contain(s => s.Contains("ENABLE ROW LEVEL SECURITY"));
result.GeneratedStatements.Should().Contain(s => s.Contains("CREATE POLICY"));
}
[Fact]
public void GenerateRlsSql_Verdict_includes_force_rls()
{
var result = _service.GenerateRlsSql(AttestorSchema.Verdict);
result.GeneratedStatements.Should().Contain(s => s.Contains("FORCE ROW LEVEL SECURITY"));
}
[Fact]
public void GenerateRlsSql_ProofChain_returns_empty_statements()
{
var result = _service.GenerateRlsSql(AttestorSchema.ProofChain);
result.Success.Should().BeTrue();
result.GeneratedStatements.Should().BeEmpty();
}
[Fact]
public void GenerateRlsSql_Watchlist_generates_multiple_policies()
{
var result = _service.GenerateRlsSql(AttestorSchema.Watchlist);
var policyStatements = result.GeneratedStatements.Where(s => s.Contains("CREATE POLICY")).ToList();
policyStatements.Should().HaveCountGreaterThan(1);
}
[Fact]
public void GenerateRlsSql_uses_permissive_mode()
{
var result = _service.GenerateRlsSql(AttestorSchema.Verdict);
result.GeneratedStatements.Should().Contain(s => s.Contains("AS PERMISSIVE"));
}
// ---------------------------------------------------------------
// GetTemporalTables
// ---------------------------------------------------------------
[Fact]
public void GetTemporalTables_returns_three_configs()
{
var tables = _service.GetTemporalTables();
tables.Should().HaveCount(3);
}
[Fact]
public void GetTemporalTables_verdict_ledger_has_seven_year_retention()
{
var tables = _service.GetTemporalTables();
var verdict = tables.First(t => t.TableName.Contains("verdict_ledger"));
verdict.Retention.Should().Be(TemporalRetention.SevenYears);
}
[Fact]
public void GetTemporalTables_noise_ledger_has_seven_year_retention()
{
var tables = _service.GetTemporalTables();
var noise = tables.First(t => t.TableName.Contains("noise_ledger"));
noise.Retention.Should().Be(TemporalRetention.SevenYears);
}
[Fact]
public void GetTemporalTables_watchlist_has_one_year_retention()
{
var tables = _service.GetTemporalTables();
var watchlist = tables.First(t => t.TableName.Contains("watched_identities"));
watchlist.Retention.Should().Be(TemporalRetention.OneYear);
}
[Fact]
public void GetTemporalTables_all_have_history_table_names()
{
var tables = _service.GetTemporalTables();
tables.Should().OnlyContain(t => t.HistoryTableName.Contains("_history"));
}
// ---------------------------------------------------------------
// GenerateTemporalTableSql
// ---------------------------------------------------------------
[Fact]
public void GenerateTemporalTableSql_generates_alter_table_for_period_columns()
{
var config = _service.GetTemporalTables().First();
var result = _service.GenerateTemporalTableSql(config);
result.Success.Should().BeTrue();
result.GeneratedStatements.Should().Contain(s =>
s.Contains("sys_period_start") && s.Contains("sys_period_end"));
}
[Fact]
public void GenerateTemporalTableSql_creates_history_table()
{
var config = _service.GetTemporalTables().First();
var result = _service.GenerateTemporalTableSql(config);
result.GeneratedStatements.Should().Contain(s =>
s.Contains("CREATE TABLE IF NOT EXISTS") && s.Contains("_history"));
}
[Fact]
public void GenerateTemporalTableSql_creates_trigger_function()
{
var config = _service.GetTemporalTables().First();
var result = _service.GenerateTemporalTableSql(config);
result.GeneratedStatements.Should().Contain(s =>
s.Contains("CREATE OR REPLACE FUNCTION") && s.Contains("RETURNS TRIGGER"));
}
[Fact]
public void GenerateTemporalTableSql_attaches_trigger()
{
var config = _service.GetTemporalTables().First();
var result = _service.GenerateTemporalTableSql(config);
result.GeneratedStatements.Should().Contain(s =>
s.Contains("CREATE TRIGGER") && s.Contains("BEFORE UPDATE OR DELETE"));
}
[Fact]
public void GenerateTemporalTableSql_includes_retention_comment()
{
var config = _service.GetTemporalTables().First();
var result = _service.GenerateTemporalTableSql(config);
result.GeneratedStatements.Should().Contain(s => s.Contains("retention:"));
}
[Fact]
public void GenerateTemporalTableSql_produces_five_statements()
{
var config = _service.GetTemporalTables().First();
var result = _service.GenerateTemporalTableSql(config);
result.GeneratedStatements.Should().HaveCount(5);
}
[Fact]
public void GenerateTemporalTableSql_null_config_throws()
{
var act = () => _service.GenerateTemporalTableSql(null!);
act.Should().Throw<ArgumentNullException>();
}
// ---------------------------------------------------------------
// GetSummary
// ---------------------------------------------------------------
[Fact]
public void GetSummary_returns_complete_summary()
{
var summary = _service.GetSummary();
summary.Assignments.Should().HaveCount(5);
summary.RlsPolicies.Should().NotBeEmpty();
summary.TemporalTables.Should().HaveCount(3);
}
[Fact]
public void GetSummary_ProvisionedCount_reflects_isProvisioned_flags()
{
var summary = _service.GetSummary();
// Default IsProvisioned is false for all assignments
summary.ProvisionedCount.Should().Be(0);
}
[Fact]
public void GetSummary_RlsEnabledCount_counts_non_disabled_policies()
{
var summary = _service.GetSummary();
// All RLS policies are Permissive (not Disabled)
summary.RlsEnabledCount.Should().Be(summary.RlsPolicies.Length);
}
[Fact]
public void GetSummary_records_timestamp()
{
var summary = _service.GetSummary();
summary.ComputedAt.Should().Be(_timeProvider.GetUtcNow());
}
// ---------------------------------------------------------------
// Null-time-provider fallback
// ---------------------------------------------------------------
[Fact]
public void Constructor_null_time_provider_uses_system_default()
{
using var mf = new TestSchemaIsolationMeterFactory();
var svc = new SchemaIsolationService(null, mf);
var result = svc.GenerateProvisioningSql(AttestorSchema.Verdict);
result.Timestamp.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
public void Constructor_null_meter_factory_throws()
{
var act = () => new SchemaIsolationService(_timeProvider, null!);
act.Should().Throw<ArgumentNullException>();
}
// ---------------------------------------------------------------
// Cross-schema consistency checks
// ---------------------------------------------------------------
[Fact]
public void RlsPolicies_only_reference_schemas_with_assignments()
{
var assignedSchemas = _service.GetAllAssignments().Select(a => a.Schema).ToHashSet();
foreach (var schema in Enum.GetValues<AttestorSchema>())
{
var policies = _service.GetRlsPolicies(schema);
foreach (var p in policies)
{
assignedSchemas.Should().Contain(p.Schema);
}
}
}
[Fact]
public void TemporalTables_only_reference_schemas_with_assignments()
{
var assignedSchemas = _service.GetAllAssignments().Select(a => a.Schema).ToHashSet();
var tables = _service.GetTemporalTables();
foreach (var t in tables)
{
assignedSchemas.Should().Contain(t.Schema);
}
}
[Fact]
public void Deterministic_provisioning_sql_for_same_schema()
{
var result1 = _service.GenerateProvisioningSql(AttestorSchema.Verdict);
var result2 = _service.GenerateProvisioningSql(AttestorSchema.Verdict);
result1.GeneratedStatements.Should().BeEquivalentTo(result2.GeneratedStatements);
}
[Fact]
public void Deterministic_rls_sql_for_same_schema()
{
var result1 = _service.GenerateRlsSql(AttestorSchema.Watchlist);
var result2 = _service.GenerateRlsSql(AttestorSchema.Watchlist);
result1.GeneratedStatements.Should().BeEquivalentTo(result2.GeneratedStatements);
}
[Fact]
public void Deterministic_temporal_sql_for_same_config()
{
var config = _service.GetTemporalTables().First();
var result1 = _service.GenerateTemporalTableSql(config);
var result2 = _service.GenerateTemporalTableSql(config);
result1.GeneratedStatements.Should().BeEquivalentTo(result2.GeneratedStatements);
}
}