audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
@@ -78,7 +78,6 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
public async Task Database_CanCreateAndVerifySchema()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
|
||||
await connection.OpenAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -101,7 +100,6 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
public async Task Database_CanPerformCrudOperations()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
|
||||
await connection.OpenAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -151,7 +149,6 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
public async Task Database_CanRunDdlMigrations()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
|
||||
await connection.OpenAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -193,7 +190,6 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
public async Task Database_CanCreateExtensions()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
|
||||
await connection.OpenAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
|
||||
@@ -10,14 +10,6 @@
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="FixtureHarvester.csproj" />
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Chaos Tests
|
||||
|
||||
## Roles
|
||||
- QA / test engineer: deterministic unit tests for chaos testing helpers.
|
||||
- Backend engineer: maintain test utilities and fixtures.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Chaos.Tests
|
||||
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Chaos
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Avoid DateTimeOffset.UtcNow and real delays in tests.
|
||||
- Prefer simulated or fake time providers for time-based behavior.
|
||||
|
||||
## Testing
|
||||
- Cover failure injection, choreography steps, and convergence tracking with edge cases.
|
||||
@@ -14,10 +14,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
24
src/__Tests/__Libraries/StellaOps.Testing.Chaos/AGENTS.md
Normal file
24
src/__Tests/__Libraries/StellaOps.Testing.Chaos/AGENTS.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Chaos Library
|
||||
|
||||
## Roles
|
||||
- Backend engineer: maintain chaos testing helpers and failure injectors.
|
||||
- QA / test engineer: ensure deterministic, repeatable chaos simulations.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Chaos
|
||||
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Chaos.Tests
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Avoid DateTimeOffset.UtcNow and Random.Shared; use TimeProvider and deterministic RNG.
|
||||
- Avoid real Task.Delay in test utilities; prefer simulated time.
|
||||
|
||||
## Testing
|
||||
- Cover failure types, registry parsing, and deterministic ordering behavior.
|
||||
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.ConfigDiff Library
|
||||
|
||||
## Roles
|
||||
- Backend engineer: maintain config-diff test helpers.
|
||||
- QA / test engineer: verify deterministic behavior snapshots and delta checks.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.ConfigDiff
|
||||
- Used by config-diff test suites in modules.
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Avoid DateTimeOffset.UtcNow in snapshots; pass explicit timestamps or TimeProvider.
|
||||
- Use InvariantCulture for value formatting when capturing behavior snapshots.
|
||||
|
||||
## Testing
|
||||
- Cover delta computation, ignored behaviors, and numeric tolerance behavior.
|
||||
24
src/__Tests/__Libraries/StellaOps.Testing.Coverage/AGENTS.md
Normal file
24
src/__Tests/__Libraries/StellaOps.Testing.Coverage/AGENTS.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Coverage Library
|
||||
|
||||
## Roles
|
||||
- Backend engineer: maintain coverage parsing and enforcement helpers.
|
||||
- QA / test engineer: verify deterministic coverage calculations and reporting.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Coverage
|
||||
- Used by test suites that enforce coverage thresholds.
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Avoid DateTimeOffset.UtcNow for report timestamps; use explicit values or TimeProvider.
|
||||
- Use invariant formatting for coverage percentages and numeric outputs.
|
||||
|
||||
## Testing
|
||||
- Cover Cobertura parsing, dead-path detection, and exemption matching.
|
||||
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Evidence Tests
|
||||
|
||||
## Roles
|
||||
- QA / test engineer: deterministic unit tests for test evidence services.
|
||||
- Backend engineer: maintain test fixtures and validation coverage.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Evidence.Tests
|
||||
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Evidence
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Avoid DateTimeOffset.UtcNow in fixtures; use FakeTimeProvider or fixed timestamps.
|
||||
- Keep deterministic ordering when asserting hashes or summaries.
|
||||
|
||||
## Testing
|
||||
- Cover finalization, bundle retrieval, and deterministic hashing paths.
|
||||
@@ -10,12 +10,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Testing.Evidence\StellaOps.Testing.Evidence.csproj" />
|
||||
|
||||
24
src/__Tests/__Libraries/StellaOps.Testing.Evidence/AGENTS.md
Normal file
24
src/__Tests/__Libraries/StellaOps.Testing.Evidence/AGENTS.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Evidence Library
|
||||
|
||||
## Roles
|
||||
- Backend engineer: maintain test evidence storage and hashing.
|
||||
- QA / test engineer: validate determinism and summary calculations.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Evidence
|
||||
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Evidence.Tests
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Use canonical JSON or stable ordering for any hash inputs.
|
||||
- Avoid nondeterministic timestamps; prefer TimeProvider.
|
||||
|
||||
## Testing
|
||||
- Cover bundle ID/merkle root determinism and summary grouping behavior.
|
||||
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Explainability Library
|
||||
|
||||
## Roles
|
||||
- Backend engineer: maintain explainability assertion helpers.
|
||||
- QA / test engineer: ensure deterministic, reproducible decision checks.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Explainability
|
||||
- Used by explainability test suites across modules.
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Use ordinal ordering when comparing factor IDs or rule names.
|
||||
- Avoid culture-sensitive formatting in assertion messages.
|
||||
|
||||
## Testing
|
||||
- Cover reproducibility checks, factor weight validation, and rule-trigger assertions.
|
||||
24
src/__Tests/__Libraries/StellaOps.Testing.Policy/AGENTS.md
Normal file
24
src/__Tests/__Libraries/StellaOps.Testing.Policy/AGENTS.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Policy Library
|
||||
|
||||
## Roles
|
||||
- Backend engineer: maintain policy diff and regression helpers.
|
||||
- QA / test engineer: validate deterministic regression checks.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Policy
|
||||
- Used by policy regression test suites across modules.
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Avoid DateTimeOffset.UtcNow in default results; use TimeProvider.
|
||||
- Use ordinal ordering for factor comparisons and diff outputs.
|
||||
|
||||
## Testing
|
||||
- Cover factor delta detection, allowed-change filtering, and diff ordering.
|
||||
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Replay Tests
|
||||
|
||||
## Roles
|
||||
- QA / test engineer: deterministic unit tests for replay helpers.
|
||||
- Backend engineer: maintain replay test fixtures and trace builders.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Replay.Tests
|
||||
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Replay
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Avoid DateTimeOffset.UtcNow in trace fixtures; use fixed timestamps.
|
||||
- Ensure trace ordering and output hashes are deterministic.
|
||||
|
||||
## Testing
|
||||
- Cover query filters, batch replay expectations, and cancellation paths.
|
||||
@@ -10,12 +10,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Testing.Replay\StellaOps.Testing.Replay.csproj" />
|
||||
|
||||
24
src/__Tests/__Libraries/StellaOps.Testing.Replay/AGENTS.md
Normal file
24
src/__Tests/__Libraries/StellaOps.Testing.Replay/AGENTS.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Replay Library
|
||||
|
||||
## Roles
|
||||
- Backend engineer: maintain replay orchestration helpers.
|
||||
- QA / test engineer: validate deterministic replay outputs.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Replay
|
||||
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Replay.Tests
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Use deterministic time providers and stable ordering for trace queries.
|
||||
- Avoid nondeterministic hashes by sorting inputs before hashing.
|
||||
|
||||
## Testing
|
||||
- Cover corpus import/query, output hash determinism, and replay batching.
|
||||
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.SchemaEvolution Library
|
||||
|
||||
## Roles
|
||||
- Backend engineer: maintain schema evolution test harnesses.
|
||||
- QA / test engineer: validate backward/forward compatibility checks.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.SchemaEvolution
|
||||
- Used by module-specific schema evolution test suites.
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Avoid DateTimeOffset.UtcNow defaults for schema timestamps.
|
||||
- Pin or configure container images for deterministic test runs.
|
||||
|
||||
## Testing
|
||||
- Cover migration rollback paths, version selection, and container lifecycle.
|
||||
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Temporal Tests
|
||||
|
||||
## Roles
|
||||
- QA / test engineer: deterministic unit tests for temporal helpers.
|
||||
- Backend engineer: maintain time simulation fixtures.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Temporal.Tests
|
||||
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Temporal
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Use fixed timestamps in fixtures; avoid DateTimeOffset.UtcNow.
|
||||
- Avoid real delays in multi-threaded tests.
|
||||
|
||||
## Testing
|
||||
- Cover drift, leap seconds, TTL boundaries, and idempotency scenarios.
|
||||
@@ -12,12 +12,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
24
src/__Tests/__Libraries/StellaOps.Testing.Temporal/AGENTS.md
Normal file
24
src/__Tests/__Libraries/StellaOps.Testing.Temporal/AGENTS.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# AGENTS - Testing.Temporal Library
|
||||
|
||||
## Roles
|
||||
- Backend engineer: maintain temporal test utilities.
|
||||
- QA / test engineer: validate deterministic time simulation.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- src/__Libraries/AGENTS.md
|
||||
- Current sprint file under docs/implplan/SPRINT_*.md
|
||||
|
||||
## Working Directory & Boundaries
|
||||
- Primary scope: src/__Tests/__Libraries/StellaOps.Testing.Temporal
|
||||
- Test target: src/__Tests/__Libraries/StellaOps.Testing.Temporal.Tests
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Avoid DateTimeOffset.UtcNow defaults; require explicit start times.
|
||||
- Use stable hashing in comparers to avoid randomized GetHashCode behavior.
|
||||
|
||||
## Testing
|
||||
- Cover leap-second smearing, idempotency comparisons, and time drift math.
|
||||
812
src/__Tests/e2e/Integrations/FacetSealAdmissionE2ETests.cs
Normal file
812
src/__Tests/e2e/Integrations/FacetSealAdmissionE2ETests.cs
Normal file
@@ -0,0 +1,812 @@
|
||||
// =============================================================================
|
||||
// FacetSealAdmissionE2ETests.cs
|
||||
// Sprint: SPRINT_20260105_002_004_CLI
|
||||
// Task: CLI-018 - E2E test: seal -> deploy -> drift check
|
||||
// Description: End-to-end tests for facet seal admission workflow
|
||||
// =============================================================================
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using StellaOps.Facet;
|
||||
using StellaOps.Integration.E2E.Integrations.Fixtures;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Integration.E2E.Integrations;
|
||||
|
||||
/// <summary>
|
||||
/// E2E tests for the facet seal admission workflow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tests cover the complete workflow:
|
||||
/// 1. Creating a facet seal for an image
|
||||
/// 2. Simulating Kubernetes admission with facet validation
|
||||
/// 3. Detecting drift between baseline and current state
|
||||
/// 4. Verifying quota-based admission decisions
|
||||
/// </remarks>
|
||||
[Trait("Category", "E2E")]
|
||||
[Trait("Category", "Integrations")]
|
||||
[Trait("Category", "Facet")]
|
||||
public class FacetSealAdmissionE2ETests : IClassFixture<IntegrationTestFixture>
|
||||
{
|
||||
private readonly IntegrationTestFixture _fixture;
|
||||
|
||||
public FacetSealAdmissionE2ETests(IntegrationTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
#region Seal Creation Tests
|
||||
|
||||
[Fact(DisplayName = "Facet seal is created with correct structure")]
|
||||
public void FacetSeal_CreatedWithCorrectStructure()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:abc123def456789012345678901234567890123456789012345678901234";
|
||||
var createdAt = DateTimeOffset.UtcNow;
|
||||
|
||||
// Act
|
||||
var seal = CreateTestSeal(imageDigest, createdAt);
|
||||
|
||||
// Assert
|
||||
seal.ImageDigest.Should().Be(imageDigest);
|
||||
seal.CreatedAt.Should().Be(createdAt);
|
||||
seal.CombinedMerkleRoot.Should().NotBeNullOrEmpty();
|
||||
seal.Facets.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Facet seal contains all built-in facets")]
|
||||
public void FacetSeal_ContainsAllBuiltInFacets()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:test123";
|
||||
var expectedFacets = new[] { "runtime", "config", "static" };
|
||||
|
||||
// Act
|
||||
var seal = CreateTestSealWithFacets(imageDigest, expectedFacets);
|
||||
|
||||
// Assert
|
||||
seal.Facets.Select(f => f.FacetId).Should().BeEquivalentTo(expectedFacets);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Facet seal Merkle root is deterministic")]
|
||||
public void FacetSeal_MerkleRootIsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:determinism-test";
|
||||
var fixedTime = new DateTimeOffset(2026, 1, 5, 12, 0, 0, TimeSpan.Zero);
|
||||
var fixedMerkleRoot = "sha256:deterministic-root-hash-for-testing";
|
||||
|
||||
// Act - Create seals with same fixed root
|
||||
var seal1 = CreateTestSealDeterministic(imageDigest, fixedTime, fixedMerkleRoot);
|
||||
var seal2 = CreateTestSealDeterministic(imageDigest, fixedTime, fixedMerkleRoot);
|
||||
|
||||
// Assert
|
||||
seal1.CombinedMerkleRoot.Should().Be(seal2.CombinedMerkleRoot);
|
||||
seal1.ImageDigest.Should().Be(seal2.ImageDigest);
|
||||
seal1.CreatedAt.Should().Be(seal2.CreatedAt);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Admission Validation Tests
|
||||
|
||||
[Fact(DisplayName = "Admission allows when facet validation not required")]
|
||||
public async Task Admission_AllowsWhenValidationNotRequired()
|
||||
{
|
||||
// Arrange
|
||||
var validator = CreateMockValidator();
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: "sha256:any-image",
|
||||
namespaceAnnotations: new Dictionary<string, string>()); // No annotation
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Allowed.Should().BeTrue();
|
||||
result.Message.Should().Contain("not required");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Admission allows when annotation is false")]
|
||||
public async Task Admission_AllowsWhenAnnotationFalse()
|
||||
{
|
||||
// Arrange
|
||||
var validator = CreateMockValidator();
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: "sha256:any-image",
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "false"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Admission denies when seal missing and required")]
|
||||
public async Task Admission_DeniesWhenSealMissingAndRequired()
|
||||
{
|
||||
// Arrange
|
||||
var mockSealStore = new Mock<IFacetSealStore>();
|
||||
mockSealStore
|
||||
.Setup(x => x.GetLatestSealAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((FacetSeal?)null);
|
||||
|
||||
var validator = CreateMockValidator(mockSealStore.Object);
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: "sha256:missing-seal",
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "true"
|
||||
},
|
||||
requireSeal: true);
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Allowed.Should().BeFalse();
|
||||
result.Code.Should().Be("facet.seal.missing");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Admission allows when seal exists and no drift")]
|
||||
public async Task Admission_AllowsWhenSealExistsNoDrift()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:sealed-image";
|
||||
var seal = CreateTestSeal(imageDigest);
|
||||
|
||||
var mockSealStore = new Mock<IFacetSealStore>();
|
||||
mockSealStore
|
||||
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(seal);
|
||||
|
||||
var validator = CreateMockValidator(mockSealStore.Object);
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: imageDigest,
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Allowed.Should().BeTrue();
|
||||
result.Message.Should().Contain("verified");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Drift Detection Tests
|
||||
|
||||
[Fact(DisplayName = "Drift detection identifies added files")]
|
||||
public async Task DriftDetection_IdentifiesAddedFiles()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:drift-test";
|
||||
var baselineSeal = CreateTestSeal(imageDigest);
|
||||
var currentSeal = CreateTestSealWithAdditionalFiles(imageDigest, 5);
|
||||
|
||||
var mockDriftDetector = new Mock<IFacetDriftDetector>();
|
||||
mockDriftDetector
|
||||
.Setup(x => x.DetectDriftAsync(baselineSeal, currentSeal, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(CreateDriftReport(QuotaVerdict.Warning, 5));
|
||||
|
||||
// Act
|
||||
var report = await mockDriftDetector.Object.DetectDriftAsync(baselineSeal, currentSeal);
|
||||
|
||||
// Assert
|
||||
report.TotalChangedFiles.Should().Be(5);
|
||||
report.OverallVerdict.Should().Be(QuotaVerdict.Warning);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Drift quota Ok allows admission")]
|
||||
public async Task DriftQuotaOk_AllowsAdmission()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:ok-drift";
|
||||
var seal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
|
||||
var currentSeal = CreateTestSeal(imageDigest, merkleRoot: "current-root");
|
||||
var driftReport = CreateDriftReport(QuotaVerdict.Ok, 2);
|
||||
|
||||
var mockSealStore = new Mock<IFacetSealStore>();
|
||||
mockSealStore
|
||||
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(seal);
|
||||
mockSealStore
|
||||
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(currentSeal);
|
||||
|
||||
var mockDriftDetector = new Mock<IFacetDriftDetector>();
|
||||
mockDriftDetector
|
||||
.Setup(x => x.DetectDriftAsync(seal, currentSeal, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(driftReport);
|
||||
|
||||
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: imageDigest,
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "true"
|
||||
},
|
||||
currentSealRoot: "current-root");
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Allowed.Should().BeTrue();
|
||||
result.Message.Should().Contain("OK");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Drift quota Warning allows with warning")]
|
||||
public async Task DriftQuotaWarning_AllowsWithWarning()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:warning-drift";
|
||||
var seal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
|
||||
var currentSeal = CreateTestSeal(imageDigest, merkleRoot: "current-root");
|
||||
var driftReport = CreateDriftReport(QuotaVerdict.Warning, 10);
|
||||
|
||||
var mockSealStore = new Mock<IFacetSealStore>();
|
||||
mockSealStore
|
||||
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(seal);
|
||||
mockSealStore
|
||||
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(currentSeal);
|
||||
|
||||
var mockDriftDetector = new Mock<IFacetDriftDetector>();
|
||||
mockDriftDetector
|
||||
.Setup(x => x.DetectDriftAsync(seal, currentSeal, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(driftReport);
|
||||
|
||||
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: imageDigest,
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "true"
|
||||
},
|
||||
currentSealRoot: "current-root");
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Allowed.Should().BeTrue();
|
||||
result.IsWarning.Should().BeTrue();
|
||||
result.Code.Should().Be("facet.quota.warning");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Drift quota Blocked denies admission")]
|
||||
public async Task DriftQuotaBlocked_DeniesAdmission()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:blocked-drift";
|
||||
var seal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
|
||||
var currentSeal = CreateTestSeal(imageDigest, merkleRoot: "current-root");
|
||||
var driftReport = CreateDriftReport(QuotaVerdict.Blocked, 50);
|
||||
|
||||
var mockSealStore = new Mock<IFacetSealStore>();
|
||||
mockSealStore
|
||||
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(seal);
|
||||
mockSealStore
|
||||
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(currentSeal);
|
||||
|
||||
var mockDriftDetector = new Mock<IFacetDriftDetector>();
|
||||
mockDriftDetector
|
||||
.Setup(x => x.DetectDriftAsync(seal, currentSeal, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(driftReport);
|
||||
|
||||
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: imageDigest,
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "true"
|
||||
},
|
||||
currentSealRoot: "current-root");
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Allowed.Should().BeFalse();
|
||||
result.Code.Should().Be("facet.quota.exceeded");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Drift RequiresVex denies admission")]
|
||||
public async Task DriftRequiresVex_DeniesAdmission()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:vex-required";
|
||||
var seal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
|
||||
var currentSeal = CreateTestSeal(imageDigest, merkleRoot: "current-root");
|
||||
var driftReport = CreateDriftReport(QuotaVerdict.RequiresVex, 30);
|
||||
|
||||
var mockSealStore = new Mock<IFacetSealStore>();
|
||||
mockSealStore
|
||||
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(seal);
|
||||
mockSealStore
|
||||
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(currentSeal);
|
||||
|
||||
var mockDriftDetector = new Mock<IFacetDriftDetector>();
|
||||
mockDriftDetector
|
||||
.Setup(x => x.DetectDriftAsync(seal, currentSeal, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(driftReport);
|
||||
|
||||
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: imageDigest,
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "true"
|
||||
},
|
||||
currentSealRoot: "current-root");
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Allowed.Should().BeFalse();
|
||||
result.Code.Should().Be("facet.vex.required");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Full Workflow Tests
|
||||
|
||||
[Fact(DisplayName = "Full workflow: seal -> deploy (no drift) -> allow")]
|
||||
public async Task FullWorkflow_SealDeployNoDrift_Allow()
|
||||
{
|
||||
// Arrange - Create seal
|
||||
var imageDigest = "sha256:workflow-test-1";
|
||||
var seal = CreateTestSeal(imageDigest);
|
||||
|
||||
// Arrange - Mock stores
|
||||
var mockSealStore = new Mock<IFacetSealStore>();
|
||||
mockSealStore
|
||||
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(seal);
|
||||
|
||||
var validator = CreateMockValidator(mockSealStore.Object);
|
||||
|
||||
// Act - Simulate deploy
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: imageDigest,
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "true"
|
||||
});
|
||||
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Full workflow: seal -> deploy (with drift) -> quota check")]
|
||||
public async Task FullWorkflow_SealDeployWithDrift_QuotaCheck()
|
||||
{
|
||||
// Arrange - Create seals (baseline and current)
|
||||
var imageDigest = "sha256:workflow-test-2";
|
||||
var baselineSeal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
|
||||
var currentSeal = CreateTestSealWithAdditionalFiles(imageDigest, 15, "current-root");
|
||||
|
||||
// Arrange - Mock stores
|
||||
var mockSealStore = new Mock<IFacetSealStore>();
|
||||
mockSealStore
|
||||
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(baselineSeal);
|
||||
mockSealStore
|
||||
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(currentSeal);
|
||||
|
||||
var mockDriftDetector = new Mock<IFacetDriftDetector>();
|
||||
mockDriftDetector
|
||||
.Setup(x => x.DetectDriftAsync(baselineSeal, currentSeal, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(CreateDriftReport(QuotaVerdict.Warning, 15));
|
||||
|
||||
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
|
||||
|
||||
// Act - Simulate deploy
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: imageDigest,
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "true"
|
||||
},
|
||||
currentSealRoot: "current-root");
|
||||
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert - Should allow with warning
|
||||
result.Allowed.Should().BeTrue();
|
||||
result.IsWarning.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Full workflow: seal -> deploy (excessive drift) -> deny")]
|
||||
public async Task FullWorkflow_SealDeployExcessiveDrift_Deny()
|
||||
{
|
||||
// Arrange - Create seals with significant drift
|
||||
var imageDigest = "sha256:workflow-test-3";
|
||||
var baselineSeal = CreateTestSeal(imageDigest, merkleRoot: "baseline-root");
|
||||
var currentSeal = CreateTestSealWithAdditionalFiles(imageDigest, 100, "current-root");
|
||||
|
||||
// Arrange - Mock stores
|
||||
var mockSealStore = new Mock<IFacetSealStore>();
|
||||
mockSealStore
|
||||
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(baselineSeal);
|
||||
mockSealStore
|
||||
.Setup(x => x.GetByCombinedRootAsync("current-root", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(currentSeal);
|
||||
|
||||
var mockDriftDetector = new Mock<IFacetDriftDetector>();
|
||||
mockDriftDetector
|
||||
.Setup(x => x.DetectDriftAsync(baselineSeal, currentSeal, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(CreateDriftReport(QuotaVerdict.Blocked, 100));
|
||||
|
||||
var validator = CreateMockValidator(mockSealStore.Object, mockDriftDetector.Object);
|
||||
|
||||
// Act - Simulate deploy
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: imageDigest,
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "true"
|
||||
},
|
||||
currentSealRoot: "current-root");
|
||||
|
||||
var result = await validator.ValidateAsync(request);
|
||||
|
||||
// Assert - Should deny
|
||||
result.Allowed.Should().BeFalse();
|
||||
result.Code.Should().Be("facet.quota.exceeded");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact(DisplayName = "Admission decisions are deterministic")]
|
||||
public async Task AdmissionDecisions_AreDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:determinism-admission";
|
||||
var seal = CreateTestSeal(imageDigest);
|
||||
|
||||
var mockSealStore = new Mock<IFacetSealStore>();
|
||||
mockSealStore
|
||||
.Setup(x => x.GetLatestSealAsync(imageDigest, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(seal);
|
||||
|
||||
var validator = CreateMockValidator(mockSealStore.Object);
|
||||
var request = CreateAdmissionRequest(
|
||||
imageDigest: imageDigest,
|
||||
namespaceAnnotations: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.io/facet-seal-required"] = "true"
|
||||
});
|
||||
|
||||
// Act
|
||||
var results = await Task.WhenAll(
|
||||
Enumerable.Range(0, 10).Select(_ => validator.ValidateAsync(request)));
|
||||
|
||||
// Assert
|
||||
results.Select(r => r.Allowed).Distinct().Should().HaveCount(1);
|
||||
results.Select(r => r.Code).Distinct().Should().HaveCount(1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static FacetSeal CreateTestSeal(
|
||||
string imageDigest,
|
||||
DateTimeOffset? createdAt = null,
|
||||
string? merkleRoot = null)
|
||||
{
|
||||
return new FacetSeal
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
CreatedAt = createdAt ?? DateTimeOffset.UtcNow,
|
||||
CombinedMerkleRoot = merkleRoot ?? $"sha256:{Guid.NewGuid():N}",
|
||||
Facets = ImmutableArray.Create(
|
||||
new FacetEntry
|
||||
{
|
||||
FacetId = "runtime",
|
||||
Name = "Runtime Binaries",
|
||||
Category = FacetCategory.Binaries,
|
||||
Selectors = ImmutableArray<string>.Empty,
|
||||
MerkleRoot = $"sha256:{Guid.NewGuid():N}",
|
||||
FileCount = 10,
|
||||
TotalBytes = 1024000,
|
||||
Files = ImmutableArray<FacetFileEntry>.Empty
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetSeal CreateTestSealDeterministic(
|
||||
string imageDigest,
|
||||
DateTimeOffset createdAt,
|
||||
string merkleRoot)
|
||||
{
|
||||
return new FacetSeal
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
CreatedAt = createdAt,
|
||||
CombinedMerkleRoot = merkleRoot,
|
||||
Facets = ImmutableArray.Create(
|
||||
new FacetEntry
|
||||
{
|
||||
FacetId = "runtime",
|
||||
Name = "Runtime Binaries",
|
||||
Category = FacetCategory.Binaries,
|
||||
Selectors = ImmutableArray<string>.Empty,
|
||||
MerkleRoot = "sha256:deterministic-facet-root",
|
||||
FileCount = 10,
|
||||
TotalBytes = 1024000,
|
||||
Files = ImmutableArray<FacetFileEntry>.Empty
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetSeal CreateTestSealWithFacets(string imageDigest, string[] facetIds)
|
||||
{
|
||||
var facets = facetIds.Select(id => new FacetEntry
|
||||
{
|
||||
FacetId = id,
|
||||
Name = $"{id} facet",
|
||||
Category = FacetCategory.Custom,
|
||||
Selectors = ImmutableArray<string>.Empty,
|
||||
MerkleRoot = $"sha256:{Guid.NewGuid():N}",
|
||||
FileCount = 5,
|
||||
TotalBytes = 51200,
|
||||
Files = ImmutableArray<FacetFileEntry>.Empty
|
||||
}).ToImmutableArray();
|
||||
|
||||
return new FacetSeal
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
CombinedMerkleRoot = $"sha256:{Guid.NewGuid():N}",
|
||||
Facets = facets
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetSeal CreateTestSealWithAdditionalFiles(
|
||||
string imageDigest,
|
||||
int additionalFileCount,
|
||||
string? merkleRoot = null)
|
||||
{
|
||||
var files = Enumerable.Range(0, additionalFileCount)
|
||||
.Select(i => new FacetFileEntry($"/added/{i}.bin", $"sha256:file{i}", 1024, null))
|
||||
.ToImmutableArray();
|
||||
|
||||
return new FacetSeal
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
CombinedMerkleRoot = merkleRoot ?? $"sha256:{Guid.NewGuid():N}",
|
||||
Facets = ImmutableArray.Create(
|
||||
new FacetEntry
|
||||
{
|
||||
FacetId = "runtime",
|
||||
Name = "Runtime Binaries",
|
||||
Category = FacetCategory.Binaries,
|
||||
Selectors = ImmutableArray<string>.Empty,
|
||||
MerkleRoot = $"sha256:{Guid.NewGuid():N}",
|
||||
FileCount = 10 + additionalFileCount,
|
||||
TotalBytes = 1024000 + (additionalFileCount * 1024),
|
||||
Files = files
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetDriftReport CreateDriftReport(QuotaVerdict verdict, int changedFiles)
|
||||
{
|
||||
var addedFiles = changedFiles > 0
|
||||
? Enumerable.Range(0, changedFiles)
|
||||
.Select(i => new FacetFileEntry($"/added/{i}", $"sha256:hash{i}", 1024, null))
|
||||
.ToImmutableArray()
|
||||
: ImmutableArray<FacetFileEntry>.Empty;
|
||||
|
||||
var facetDrifts = changedFiles > 0
|
||||
? ImmutableArray.Create(new FacetDrift
|
||||
{
|
||||
FacetId = "runtime",
|
||||
BaselineFileCount = 10,
|
||||
Added = addedFiles,
|
||||
Removed = ImmutableArray<FacetFileEntry>.Empty,
|
||||
Modified = ImmutableArray<FacetFileModification>.Empty,
|
||||
DriftScore = changedFiles * 10m,
|
||||
QuotaVerdict = verdict
|
||||
})
|
||||
: ImmutableArray<FacetDrift>.Empty;
|
||||
|
||||
return new FacetDriftReport
|
||||
{
|
||||
ImageDigest = "sha256:test",
|
||||
BaselineSealId = "baseline",
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
OverallVerdict = verdict,
|
||||
FacetDrifts = facetDrifts
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetAdmissionRequest CreateAdmissionRequest(
|
||||
string imageDigest,
|
||||
Dictionary<string, string> namespaceAnnotations,
|
||||
bool requireSeal = false,
|
||||
string? currentSealRoot = null)
|
||||
{
|
||||
return new FacetAdmissionRequest
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
NamespaceAnnotations = namespaceAnnotations,
|
||||
RequireSeal = requireSeal,
|
||||
CurrentSealRoot = currentSealRoot
|
||||
};
|
||||
}
|
||||
|
||||
private static IFacetAdmissionValidator CreateMockValidator(
|
||||
IFacetSealStore? sealStore = null,
|
||||
IFacetDriftDetector? driftDetector = null)
|
||||
{
|
||||
sealStore ??= new Mock<IFacetSealStore>().Object;
|
||||
driftDetector ??= new Mock<IFacetDriftDetector>().Object;
|
||||
|
||||
return new FacetAdmissionValidator(sealStore, driftDetector);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
// Request model for E2E tests (mirrors production model)
|
||||
internal sealed record FacetAdmissionRequest
|
||||
{
|
||||
public required string ImageDigest { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? NamespaceAnnotations { get; init; }
|
||||
public bool RequireSeal { get; init; }
|
||||
public string? CurrentSealRoot { get; init; }
|
||||
}
|
||||
|
||||
// Result model for E2E tests (mirrors production model)
|
||||
internal sealed record FacetAdmissionResult
|
||||
{
|
||||
public required bool Allowed { get; init; }
|
||||
public bool IsWarning { get; init; }
|
||||
public string? Code { get; init; }
|
||||
public required string Message { get; init; }
|
||||
|
||||
public static FacetAdmissionResult Allow(string message)
|
||||
=> new() { Allowed = true, Message = message };
|
||||
|
||||
public static FacetAdmissionResult AllowWithWarning(string code, string message)
|
||||
=> new() { Allowed = true, IsWarning = true, Code = code, Message = message };
|
||||
|
||||
public static FacetAdmissionResult Deny(string code, string message)
|
||||
=> new() { Allowed = false, Code = code, Message = message };
|
||||
}
|
||||
|
||||
// Validator interface for E2E tests
|
||||
internal interface IFacetAdmissionValidator
|
||||
{
|
||||
Task<FacetAdmissionResult> ValidateAsync(
|
||||
FacetAdmissionRequest request,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
// Validator implementation for E2E tests
|
||||
internal sealed class FacetAdmissionValidator : IFacetAdmissionValidator
|
||||
{
|
||||
private readonly IFacetSealStore _sealStore;
|
||||
private readonly IFacetDriftDetector _driftDetector;
|
||||
|
||||
public const string FacetSealRequiredAnnotation = "stellaops.io/facet-seal-required";
|
||||
|
||||
public FacetAdmissionValidator(
|
||||
IFacetSealStore sealStore,
|
||||
IFacetDriftDetector driftDetector)
|
||||
{
|
||||
_sealStore = sealStore;
|
||||
_driftDetector = driftDetector;
|
||||
}
|
||||
|
||||
public async Task<FacetAdmissionResult> ValidateAsync(
|
||||
FacetAdmissionRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!IsFacetValidationRequired(request.NamespaceAnnotations))
|
||||
{
|
||||
return FacetAdmissionResult.Allow("Facet validation not required for namespace");
|
||||
}
|
||||
|
||||
var seal = await _sealStore.GetLatestSealAsync(request.ImageDigest, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (seal is null)
|
||||
{
|
||||
if (request.RequireSeal)
|
||||
{
|
||||
return FacetAdmissionResult.Deny(
|
||||
"facet.seal.missing",
|
||||
$"No facet seal found for image {request.ImageDigest}");
|
||||
}
|
||||
return FacetAdmissionResult.Allow("No seal found, seal not required");
|
||||
}
|
||||
|
||||
if (request.CurrentSealRoot is null)
|
||||
{
|
||||
return FacetAdmissionResult.Allow(
|
||||
$"Facet seal verified: {TruncateHash(seal.CombinedMerkleRoot)}");
|
||||
}
|
||||
|
||||
var currentSeal = await _sealStore.GetByCombinedRootAsync(request.CurrentSealRoot, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (currentSeal is null)
|
||||
{
|
||||
return FacetAdmissionResult.AllowWithWarning(
|
||||
"facet.seal.current_not_found",
|
||||
$"Current seal not found, using baseline: {TruncateHash(seal.CombinedMerkleRoot)}");
|
||||
}
|
||||
|
||||
var driftReport = await _driftDetector.DetectDriftAsync(seal, currentSeal, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return driftReport.OverallVerdict switch
|
||||
{
|
||||
QuotaVerdict.Ok => FacetAdmissionResult.Allow(
|
||||
$"Facet quotas OK: {driftReport.TotalChangedFiles} changes within limits"),
|
||||
|
||||
QuotaVerdict.Warning => FacetAdmissionResult.AllowWithWarning(
|
||||
"facet.quota.warning",
|
||||
$"Facet drift warning: {FormatBreaches(driftReport)}"),
|
||||
|
||||
QuotaVerdict.Blocked => FacetAdmissionResult.Deny(
|
||||
"facet.quota.exceeded",
|
||||
$"Facet quota exceeded: {FormatBreaches(driftReport)}"),
|
||||
|
||||
QuotaVerdict.RequiresVex => FacetAdmissionResult.Deny(
|
||||
"facet.vex.required",
|
||||
$"Facet drift requires VEX authorization: {FormatBreaches(driftReport)}"),
|
||||
|
||||
_ => FacetAdmissionResult.Allow("Unknown verdict - allowing")
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsFacetValidationRequired(IReadOnlyDictionary<string, string>? annotations)
|
||||
{
|
||||
if (annotations is null) return false;
|
||||
return annotations.TryGetValue(FacetSealRequiredAnnotation, out var value) &&
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string FormatBreaches(FacetDriftReport report)
|
||||
{
|
||||
var breaches = report.FacetDrifts
|
||||
.Where(d => d.QuotaVerdict != QuotaVerdict.Ok)
|
||||
.Select(d => $"{d.FacetId}({d.ChurnPercent:F1}%)")
|
||||
.ToArray();
|
||||
return string.Join(", ", breaches);
|
||||
}
|
||||
|
||||
private static string TruncateHash(string? hash)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hash)) return "(none)";
|
||||
return hash.Length > 16 ? $"{hash[..8]}...{hash[^8..]}" : hash;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,9 @@
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Testing.Determinism\StellaOps.Testing.Determinism.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Testing.AirGap\StellaOps.Testing.AirGap.csproj" />
|
||||
|
||||
<!-- Facet seal admission E2E tests (SPRINT_20260105_002_004_CLI) -->
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Facet\StellaOps.Facet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user