// // Copyright (c) StellaOps. Licensed under BUSL-1.1. // // 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; /// /// 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. /// [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 }