audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
25
src/__Libraries/StellaOps.Facet.Tests/AGENTS.md
Normal file
25
src/__Libraries/StellaOps.Facet.Tests/AGENTS.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# AGENTS - Facet Tests
|
||||
|
||||
## Roles
|
||||
- QA / test engineer: deterministic tests for facet extraction, drift, and VEX workflows.
|
||||
- Backend engineer: align tests with facet library contracts 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/__Libraries/StellaOps.Facet.Tests
|
||||
- Test target: src/__Libraries/StellaOps.Facet
|
||||
- Avoid cross-module edits unless explicitly noted in the sprint file.
|
||||
|
||||
## Determinism and Safety
|
||||
- Use fixed timestamps and deterministic file paths in test data.
|
||||
- Avoid Guid.NewGuid in fixtures unless required for isolation.
|
||||
|
||||
## Testing
|
||||
- Cover drift detection, merkle roots, extraction filters, and VEX draft workflows.
|
||||
- Add negative cases for invalid inputs and ensure deterministic golden vectors.
|
||||
@@ -0,0 +1,302 @@
|
||||
// <copyright file="FacetQuotaVexWorkflowE2ETests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
// Sprint: SPRINT_20260105_002_003_FACET (QTA-024)
|
||||
// Description: E2E test: Quota breach -> VEX draft -> approval workflow
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Facet.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end tests for the facet quota breach to VEX approval workflow.
|
||||
/// Tests the complete pipeline: quota breach detection -> VEX draft emission -> draft approval.
|
||||
/// </summary>
|
||||
[Trait("Category", "E2E")]
|
||||
public sealed class FacetQuotaVexWorkflowE2ETests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly InMemoryFacetDriftVexDraftStore _draftStore;
|
||||
private readonly FacetDriftVexEmitter _vexEmitter;
|
||||
private readonly FacetDriftVexWorkflow _workflow;
|
||||
|
||||
public FacetQuotaVexWorkflowE2ETests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero));
|
||||
_draftStore = new InMemoryFacetDriftVexDraftStore(_timeProvider);
|
||||
_vexEmitter = new FacetDriftVexEmitter(FacetDriftVexEmitterOptions.Default, _timeProvider);
|
||||
_workflow = new FacetDriftVexWorkflow(_vexEmitter, _draftStore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task E2E_QuotaBreach_GeneratesVexDraft_CanBeApproved()
|
||||
{
|
||||
// Arrange - Create a drift report with quota breach requiring VEX
|
||||
var driftReport = CreateQuotaBreachingDriftReport(
|
||||
imageDigest: "sha256:e2e-test-image",
|
||||
facetId: "os-packages-dpkg",
|
||||
churnPercent: 35m);
|
||||
|
||||
// Act - Step 1: Process drift and emit VEX drafts
|
||||
var workflowResult = await _workflow.ExecuteAsync(driftReport, skipExisting: true, CancellationToken.None);
|
||||
|
||||
// Assert - Step 1: VEX draft was created
|
||||
Assert.True(workflowResult.Success);
|
||||
Assert.Equal(1, workflowResult.NewDraftsCreated);
|
||||
Assert.Single(workflowResult.CreatedDraftIds);
|
||||
var draftId = workflowResult.CreatedDraftIds[0];
|
||||
|
||||
// Verify draft was stored
|
||||
var storedDraft = await _draftStore.FindByIdAsync(draftId, CancellationToken.None);
|
||||
Assert.NotNull(storedDraft);
|
||||
Assert.True(storedDraft.RequiresReview);
|
||||
|
||||
// Verify draft has pending status
|
||||
var allDrafts = _draftStore.GetAllForTesting();
|
||||
var draftWithReview = allDrafts.First(d => d.Draft.DraftId == draftId);
|
||||
Assert.Equal(FacetDriftVexReviewStatus.Pending, draftWithReview.ReviewStatus);
|
||||
|
||||
// Act - Step 2: Approve the draft
|
||||
var approvalResult = await _workflow.ApproveAsync(
|
||||
draftId,
|
||||
reviewedBy: "security-team@example.com",
|
||||
notes: "Reviewed file changes, approved as authorized security patch",
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert - Step 2: Draft was approved
|
||||
Assert.True(approvalResult);
|
||||
var approvedDraftWrapper = _draftStore.GetAllForTesting().First(d => d.Draft.DraftId == draftId);
|
||||
Assert.Equal(FacetDriftVexReviewStatus.Approved, approvedDraftWrapper.ReviewStatus);
|
||||
Assert.Equal("security-team@example.com", approvedDraftWrapper.ReviewedBy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task E2E_MultipleFacetBreaches_AllGenerateDrafts()
|
||||
{
|
||||
// Arrange - Multiple facets exceeding quotas
|
||||
var facetDrifts = new[]
|
||||
{
|
||||
CreateFacetDrift("os-packages-dpkg", QuotaVerdict.RequiresVex, 25m),
|
||||
CreateFacetDrift("lang-deps-npm", QuotaVerdict.RequiresVex, 30m),
|
||||
CreateFacetDrift("binaries-usr", QuotaVerdict.Ok, 2m) // OK, should not generate draft
|
||||
};
|
||||
|
||||
var driftReport = new FacetDriftReport
|
||||
{
|
||||
ImageDigest = "sha256:multi-facet-test",
|
||||
BaselineSealId = "seal-multi-001",
|
||||
AnalyzedAt = _timeProvider.GetUtcNow(),
|
||||
FacetDrifts = [.. facetDrifts],
|
||||
OverallVerdict = QuotaVerdict.RequiresVex
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _workflow.ExecuteAsync(driftReport, skipExisting: true, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(2, result.NewDraftsCreated);
|
||||
Assert.Equal(2, result.CreatedDraftIds.Length);
|
||||
|
||||
// Verify correct facets generated drafts
|
||||
var allDrafts = _draftStore.GetAllForTesting();
|
||||
Assert.Contains(allDrafts, d => d.Draft.FacetId == "os-packages-dpkg");
|
||||
Assert.Contains(allDrafts, d => d.Draft.FacetId == "lang-deps-npm");
|
||||
Assert.DoesNotContain(allDrafts, d => d.Draft.FacetId == "binaries-usr");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task E2E_DraftRejection_MarksAsRejected()
|
||||
{
|
||||
// Arrange
|
||||
var driftReport = CreateQuotaBreachingDriftReport(
|
||||
imageDigest: "sha256:rejection-test",
|
||||
facetId: "suspicious-facet",
|
||||
churnPercent: 80m);
|
||||
|
||||
var workflowResult = await _workflow.ExecuteAsync(driftReport, skipExisting: true, CancellationToken.None);
|
||||
var draftId = workflowResult.CreatedDraftIds[0];
|
||||
|
||||
// Act - Reject the draft
|
||||
var result = await _workflow.RejectAsync(
|
||||
draftId,
|
||||
reviewedBy: "security-team@example.com",
|
||||
reason: "Unauthorized change detected - requires investigation",
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
var rejectedDraft = _draftStore.GetAllForTesting().First(d => d.Draft.DraftId == draftId);
|
||||
Assert.Equal(FacetDriftVexReviewStatus.Rejected, rejectedDraft.ReviewStatus);
|
||||
Assert.Equal("security-team@example.com", rejectedDraft.ReviewedBy);
|
||||
Assert.Contains("Unauthorized", rejectedDraft.ReviewNotes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task E2E_DraftExpiration_AfterTtl()
|
||||
{
|
||||
// Arrange - Use shorter TTL for testing
|
||||
var shortTtlOptions = new FacetDriftVexEmitterOptions { DraftTtl = TimeSpan.FromDays(3) };
|
||||
var shortTtlEmitter = new FacetDriftVexEmitter(shortTtlOptions, _timeProvider);
|
||||
var shortTtlWorkflow = new FacetDriftVexWorkflow(shortTtlEmitter, _draftStore);
|
||||
|
||||
var driftReport = CreateQuotaBreachingDriftReport(
|
||||
imageDigest: "sha256:expiry-test-short-ttl",
|
||||
facetId: "test-facet",
|
||||
churnPercent: 20m);
|
||||
|
||||
var workflowResult = await shortTtlWorkflow.ExecuteAsync(driftReport, skipExisting: true, CancellationToken.None);
|
||||
var draftId = workflowResult.CreatedDraftIds[0];
|
||||
|
||||
// Get the draft's expiration time
|
||||
var draft = await _draftStore.FindByIdAsync(draftId, CancellationToken.None);
|
||||
Assert.NotNull(draft);
|
||||
var expiresAt = draft.ExpiresAt;
|
||||
|
||||
// Act - Advance time past TTL (3 days + 1 day buffer)
|
||||
_timeProvider.Advance(TimeSpan.FromDays(4));
|
||||
|
||||
// Assert - Draft's ExpiresAt should be before current time
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
Assert.True(expiresAt < now, $"Draft should be expired: ExpiresAt={expiresAt}, Now={now}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task E2E_OverdueDrafts_CanBeQueried()
|
||||
{
|
||||
// Arrange - Use short review SLA for testing
|
||||
var shortSlaOptions = new FacetDriftVexEmitterOptions { ReviewSlaDays = 2 };
|
||||
var shortSlaEmitter = new FacetDriftVexEmitter(shortSlaOptions, _timeProvider);
|
||||
var shortSlaWorkflow = new FacetDriftVexWorkflow(shortSlaEmitter, _draftStore);
|
||||
|
||||
var driftReport = CreateQuotaBreachingDriftReport(
|
||||
imageDigest: "sha256:overdue-test-short-sla",
|
||||
facetId: "overdue-facet",
|
||||
churnPercent: 25m);
|
||||
|
||||
await shortSlaWorkflow.ExecuteAsync(driftReport, skipExisting: true, CancellationToken.None);
|
||||
|
||||
// Act - Advance time past review deadline (2 days + 1 day buffer)
|
||||
_timeProvider.Advance(TimeSpan.FromDays(3));
|
||||
|
||||
// Query using the store directly with advanced time
|
||||
var asOf = _timeProvider.GetUtcNow();
|
||||
var overdueDrafts = await _draftStore.GetOverdueAsync(asOf, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Single(overdueDrafts);
|
||||
Assert.Equal("sha256:overdue-test-short-sla", overdueDrafts[0].ImageDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task E2E_DraftContainsAuditTrail()
|
||||
{
|
||||
// Arrange
|
||||
var driftReport = CreateQuotaBreachingDriftReport(
|
||||
imageDigest: "sha256:audit-test",
|
||||
facetId: "audited-facet",
|
||||
churnPercent: 25m);
|
||||
|
||||
// Act
|
||||
var workflowResult = await _workflow.ExecuteAsync(driftReport, skipExisting: true, CancellationToken.None);
|
||||
var draftId = workflowResult.CreatedDraftIds[0];
|
||||
var draft = await _draftStore.FindByIdAsync(draftId, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(draft);
|
||||
Assert.Equal("sha256:audit-test", draft.ImageDigest);
|
||||
Assert.NotNull(draft.DriftSummary);
|
||||
Assert.Equal(25m, draft.DriftSummary.ChurnPercent);
|
||||
Assert.NotEmpty(draft.EvidenceLinks);
|
||||
Assert.Contains(draft.EvidenceLinks, l => l.Type == "facet_drift_analysis");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task E2E_PendingDraftsCanBeQueried()
|
||||
{
|
||||
// Arrange - Create multiple drafts
|
||||
var driftReport1 = CreateQuotaBreachingDriftReport("sha256:pending-test-image-001", "facet-1", 25m);
|
||||
var driftReport2 = CreateQuotaBreachingDriftReport("sha256:pending-test-image-002", "facet-2", 30m);
|
||||
|
||||
await _workflow.ExecuteAsync(driftReport1, skipExisting: true, CancellationToken.None);
|
||||
await _workflow.ExecuteAsync(driftReport2, skipExisting: true, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var pendingDrafts = await _workflow.GetPendingDraftsAsync(ct: CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, pendingDrafts.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task E2E_SkipExistingDrafts_PreventsDuplicates()
|
||||
{
|
||||
// Arrange
|
||||
var driftReport = CreateQuotaBreachingDriftReport(
|
||||
imageDigest: "sha256:duplicate-test",
|
||||
facetId: "test-facet",
|
||||
churnPercent: 20m);
|
||||
|
||||
// Act - Execute workflow twice
|
||||
var result1 = await _workflow.ExecuteAsync(driftReport, skipExisting: true, CancellationToken.None);
|
||||
var result2 = await _workflow.ExecuteAsync(driftReport, skipExisting: true, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, result1.NewDraftsCreated);
|
||||
Assert.Equal(0, result2.NewDraftsCreated);
|
||||
Assert.Equal(1, result2.ExistingDraftsSkipped);
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private FacetDriftReport CreateQuotaBreachingDriftReport(string imageDigest, string facetId, decimal churnPercent)
|
||||
{
|
||||
var addedCount = (int)churnPercent;
|
||||
var addedFiles = Enumerable.Range(0, addedCount)
|
||||
.Select(i => new FacetFileEntry($"/pkg/added{i}.deb", $"sha256:added{i}", 1024, null))
|
||||
.ToImmutableArray();
|
||||
|
||||
var facetDrift = new FacetDrift
|
||||
{
|
||||
FacetId = facetId,
|
||||
Added = addedFiles,
|
||||
Removed = [],
|
||||
Modified = [],
|
||||
DriftScore = churnPercent,
|
||||
QuotaVerdict = QuotaVerdict.RequiresVex,
|
||||
BaselineFileCount = 100
|
||||
};
|
||||
|
||||
return new FacetDriftReport
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
BaselineSealId = $"seal-{imageDigest.Substring(7, 8)}",
|
||||
AnalyzedAt = _timeProvider.GetUtcNow(),
|
||||
FacetDrifts = [facetDrift],
|
||||
OverallVerdict = QuotaVerdict.RequiresVex
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetDrift CreateFacetDrift(string facetId, QuotaVerdict verdict, decimal churnPercent)
|
||||
{
|
||||
var addedCount = (int)churnPercent;
|
||||
var addedFiles = Enumerable.Range(0, addedCount)
|
||||
.Select(i => new FacetFileEntry($"/{facetId}/file{i}", $"sha256:{facetId}{i}", 100, null))
|
||||
.ToImmutableArray();
|
||||
|
||||
return new FacetDrift
|
||||
{
|
||||
FacetId = facetId,
|
||||
Added = addedFiles,
|
||||
Removed = [],
|
||||
Modified = [],
|
||||
DriftScore = churnPercent,
|
||||
QuotaVerdict = verdict,
|
||||
BaselineFileCount = 100
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -13,8 +13,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user