notify doctors work, audit work, new product advisory sprints

This commit is contained in:
master
2026-01-13 08:36:29 +02:00
parent b8868a5f13
commit 9ca7cb183e
343 changed files with 24492 additions and 3544 deletions

View File

@@ -24,7 +24,8 @@ public sealed class ActionablesEndpointsTests
[Fact]
public async Task GetDeltaActionables_ValidDeltaId_ReturnsActionables()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678");
@@ -40,7 +41,8 @@ public sealed class ActionablesEndpointsTests
[Fact]
public async Task GetDeltaActionables_SortedByPriority()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678");
@@ -58,7 +60,8 @@ public sealed class ActionablesEndpointsTests
[Fact]
public async Task GetActionablesByPriority_Critical_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-priority/critical");
@@ -73,7 +76,8 @@ public sealed class ActionablesEndpointsTests
[Fact]
public async Task GetActionablesByPriority_InvalidPriority_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-priority/invalid");
@@ -84,7 +88,8 @@ public sealed class ActionablesEndpointsTests
[Fact]
public async Task GetActionablesByType_Upgrade_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/upgrade");
@@ -99,7 +104,8 @@ public sealed class ActionablesEndpointsTests
[Fact]
public async Task GetActionablesByType_Vex_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/vex");
@@ -114,7 +120,8 @@ public sealed class ActionablesEndpointsTests
[Fact]
public async Task GetActionablesByType_InvalidType_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/invalid");
@@ -125,7 +132,8 @@ public sealed class ActionablesEndpointsTests
[Fact]
public async Task GetDeltaActionables_IncludesEstimatedEffort()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678");

View File

@@ -18,27 +18,27 @@ namespace StellaOps.Scanner.WebService.Tests;
[Trait("Category", "Integration")]
[Trait("Sprint", "3801.0001")]
public sealed class ApprovalEndpointsTests : IDisposable
public sealed class ApprovalEndpointsTests : IAsyncLifetime
{
private readonly TestSurfaceSecretsScope _secrets;
private readonly ScannerApplicationFactory _factory;
private readonly HttpClient _client;
private TestSurfaceSecretsScope _secrets = null!;
private ScannerApplicationFactory _factory = null!;
private HttpClient _client = null!;
public ApprovalEndpointsTests()
public async Task InitializeAsync()
{
_secrets = new TestSurfaceSecretsScope();
// Use default factory without auth overrides - same pattern as ManifestEndpointsTests
// The factory defaults to anonymous auth which allows all policy assertions
_factory = new ScannerApplicationFactory();
await _factory.InitializeAsync();
_client = _factory.CreateClient();
}
public void Dispose()
public async Task DisposeAsync()
{
_client.Dispose();
_factory.Dispose();
await _factory.DisposeAsync();
_secrets.Dispose();
}

View File

@@ -10,7 +10,7 @@ public sealed class AuthorizationTests
[Fact]
public async Task ApiRoutesRequireAuthenticationWhenAuthorityEnabled()
{
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "true";
configuration["scanner:authority:allowAnonymousFallback"] = "false";
@@ -19,6 +19,7 @@ public sealed class AuthorizationTests
configuration["scanner:authority:clientId"] = "scanner-web";
configuration["scanner:authority:clientSecret"] = "secret";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/__auth-probe");

View File

@@ -25,7 +25,8 @@ public sealed class BaselineEndpointsTests
[Fact]
public async Task GetRecommendations_ValidDigest_ReturnsRecommendations()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123", TestContext.Current.CancellationToken);
@@ -42,7 +43,8 @@ public sealed class BaselineEndpointsTests
[Fact]
public async Task GetRecommendations_WithEnvironment_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123?environment=production", TestContext.Current.CancellationToken);
@@ -57,7 +59,8 @@ public sealed class BaselineEndpointsTests
[Fact]
public async Task GetRecommendations_IncludesRationale()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123", TestContext.Current.CancellationToken);
@@ -76,7 +79,8 @@ public sealed class BaselineEndpointsTests
[Fact]
public async Task GetRationale_ValidDigests_ReturnsDetailedRationale()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/rationale/sha256:base123/sha256:head456");
@@ -95,7 +99,8 @@ public sealed class BaselineEndpointsTests
[Fact]
public async Task GetRationale_IncludesSelectionCriteria()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/rationale/sha256:baseline-base123/sha256:head456");
@@ -110,7 +115,8 @@ public sealed class BaselineEndpointsTests
[Fact]
public async Task GetRecommendations_DefaultIsFirst()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123", TestContext.Current.CancellationToken);

View File

@@ -3,6 +3,8 @@
// Task: TRI-MASTER-0007 - Performance benchmark suite (TTFS)
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
@@ -18,10 +20,10 @@ namespace StellaOps.Scanner.WebService.Tests.Benchmarks;
/// TTFS (Time-To-First-Signal) performance benchmarks for triage workflows.
/// Measures the latency from request initiation to first meaningful evidence display.
///
/// Target KPIs (from Triage Advisory §3):
/// Target KPIs (from Triage Advisory section 3):
/// - TTFS p95 < 1.5s (with 100ms RTT, 1% loss)
/// - Clicks-to-Closure median < 6 clicks
/// - Evidence Completeness 90%
/// - Evidence Completeness >= 90%
/// </summary>
[Config(typeof(TtfsBenchmarkConfig))]
[MemoryDiagnoser]
@@ -149,7 +151,7 @@ public sealed class TtfsPerformanceTests
{
// Arrange
var cache = new MockEvidenceCache();
var alertId = Guid.NewGuid().ToString();
const string alertId = "alert-test-0001";
// Act
var sw = Stopwatch.StartNew();
@@ -189,7 +191,7 @@ public sealed class TtfsPerformanceTests
{
// Arrange
var cache = new MockEvidenceCache();
var alertId = Guid.NewGuid().ToString();
const string alertId = "alert-test-0002";
var evidence = cache.GetEvidence(alertId);
// Act
@@ -235,7 +237,7 @@ public sealed class TtfsPerformanceTests
{
// Arrange
var cache = new MockEvidenceCache();
var alertId = Guid.NewGuid().ToString();
const string alertId = "alert-test-0003";
// Act
var evidence = cache.GetEvidence(alertId);
@@ -290,11 +292,11 @@ public sealed class MockAlertDataStore
_alerts = Enumerable.Range(0, alertCount)
.Select(i => new Alert
{
Id = Guid.NewGuid().ToString(),
Id = $"alert-{i:D6}",
CveId = $"CVE-2024-{10000 + i}",
Severity = _random.Next(0, 4) switch { 0 => "LOW", 1 => "MEDIUM", 2 => "HIGH", _ => "CRITICAL" },
Status = "open",
CreatedAt = DateTime.UtcNow.AddDays(-_random.Next(1, 30))
CreatedAt = TtfsTestClock.FixedUtc.AddDays(-_random.Next(1, 30))
})
.ToList();
}
@@ -303,9 +305,6 @@ public sealed class MockAlertDataStore
public AlertListResult GetAlerts(int page, int pageSize)
{
// Simulate DB query latency
Thread.Sleep(5);
var skip = (page - 1) * pageSize;
return new AlertListResult
{
@@ -318,14 +317,12 @@ public sealed class MockAlertDataStore
public Alert GetAlert(string id)
{
Thread.Sleep(2);
return _alerts.First(a => a.Id == id);
}
public DecisionResult RecordDecision(string alertId, DecisionRequest request)
{
Thread.Sleep(3);
return new DecisionResult { Success = true, DecisionId = Guid.NewGuid().ToString() };
return new DecisionResult { Success = true, DecisionId = $"decision-{alertId}" };
}
}
@@ -333,9 +330,6 @@ public sealed class MockEvidenceCache
{
public EvidenceBundle GetEvidence(string alertId)
{
// Simulate evidence retrieval latency
Thread.Sleep(10);
return new EvidenceBundle
{
AlertId = alertId,
@@ -357,7 +351,7 @@ public sealed class MockEvidenceCache
VexStatus = new VexStatusEvidence
{
Status = "under_investigation",
LastUpdated = DateTime.UtcNow.AddDays(-2)
LastUpdated = TtfsTestClock.FixedUtc.AddDays(-2)
},
GraphRevision = new GraphRevisionEvidence
{
@@ -374,16 +368,23 @@ public static class ReplayTokenGenerator
public static ReplayToken Generate(string alertId, EvidenceBundle evidence)
{
// Simulate token generation
var hash = $"{alertId}:{evidence.Reachability?.Tier}:{evidence.VexStatus?.Status}".GetHashCode();
var input = $"{alertId}:{evidence.Reachability?.Tier}:{evidence.VexStatus?.Status}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
var hex = Convert.ToHexString(hash).ToLowerInvariant();
return new ReplayToken
{
Token = $"replay_{Math.Abs(hash):x8}",
Token = $"replay_{hex[..8]}",
AlertId = alertId,
GeneratedAt = DateTime.UtcNow
GeneratedAt = TtfsTestClock.FixedUtc
};
}
}
internal static class TtfsTestClock
{
public static readonly DateTime FixedUtc = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
}
#endregion
#region Models

View File

@@ -14,10 +14,11 @@ public sealed class CallGraphEndpointsTests
public async Task SubmitCallGraphRequiresContentDigestHeader()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -34,10 +35,11 @@ public sealed class CallGraphEndpointsTests
public async Task SubmitCallGraphReturnsAcceptedAndDetectsDuplicates()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();

View File

@@ -9,6 +9,7 @@ using FluentAssertions;
using StellaOps.TestKit;
using StellaOps.TestKit.Fixtures;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Scanner.WebService.Tests.Contract;
@@ -22,10 +23,12 @@ public sealed class ScannerOpenApiContractTests : IClassFixture<ScannerApplicati
{
private readonly ScannerApplicationFactory _factory;
private readonly string _snapshotPath;
private readonly ITestOutputHelper _output;
public ScannerOpenApiContractTests(ScannerApplicationFactory factory)
public ScannerOpenApiContractTests(ScannerApplicationFactory factory, ITestOutputHelper output)
{
_factory = factory;
_output = output;
_snapshotPath = Path.Combine(AppContext.BaseDirectory, "Contract", "Expected", "scanner-openapi.json");
}
@@ -79,10 +82,10 @@ public sealed class ScannerOpenApiContractTests : IClassFixture<ScannerApplicati
// Log non-breaking changes for awareness
if (changes.NonBreakingChanges.Count > 0)
{
Console.WriteLine("Non-breaking API changes detected:");
_output.WriteLine("Non-breaking API changes detected:");
foreach (var change in changes.NonBreakingChanges)
{
Console.WriteLine($" + {change}");
_output.WriteLine($" + {change}");
}
}
}

View File

@@ -25,7 +25,8 @@ public sealed class CounterfactualEndpointsTests
[Fact]
public async Task PostCompute_ValidRequest_ReturnsCounterfactuals()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
@@ -52,7 +53,8 @@ public sealed class CounterfactualEndpointsTests
[Fact]
public async Task PostCompute_MissingFindingId_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
@@ -69,7 +71,8 @@ public sealed class CounterfactualEndpointsTests
[Fact]
public async Task PostCompute_IncludesVexPath()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
@@ -90,7 +93,8 @@ public sealed class CounterfactualEndpointsTests
[Fact]
public async Task PostCompute_IncludesReachabilityPath()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
@@ -111,7 +115,8 @@ public sealed class CounterfactualEndpointsTests
[Fact]
public async Task PostCompute_IncludesExceptionPath()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
@@ -132,7 +137,8 @@ public sealed class CounterfactualEndpointsTests
[Fact]
public async Task PostCompute_WithMaxPaths_LimitsResults()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto
@@ -154,7 +160,8 @@ public sealed class CounterfactualEndpointsTests
[Fact]
public async Task GetForFinding_ValidId_ReturnsCounterfactuals()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/counterfactuals/finding/finding-123");
@@ -169,7 +176,8 @@ public sealed class CounterfactualEndpointsTests
[Fact]
public async Task GetScanSummary_ValidId_ReturnsSummary()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/counterfactuals/scan/scan-123/summary");
@@ -185,7 +193,8 @@ public sealed class CounterfactualEndpointsTests
[Fact]
public async Task GetScanSummary_IncludesPathCounts()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/counterfactuals/scan/scan-123/summary");
@@ -203,7 +212,8 @@ public sealed class CounterfactualEndpointsTests
[Fact]
public async Task PostCompute_PathsHaveConditions()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new CounterfactualRequestDto

View File

@@ -25,7 +25,8 @@ public sealed class DeltaCompareEndpointsTests
[Fact]
public async Task PostCompare_ValidRequest_ReturnsComparisonResult()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new DeltaCompareRequestDto
@@ -54,7 +55,8 @@ public sealed class DeltaCompareEndpointsTests
[Fact]
public async Task PostCompare_MissingBaseDigest_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new DeltaCompareRequestDto
@@ -71,7 +73,8 @@ public sealed class DeltaCompareEndpointsTests
[Fact]
public async Task PostCompare_MissingTargetDigest_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new DeltaCompareRequestDto
@@ -88,7 +91,8 @@ public sealed class DeltaCompareEndpointsTests
[Fact]
public async Task GetQuickDiff_ValidDigests_ReturnsQuickSummary()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/delta/quick?baseDigest=sha256:base123&targetDigest=sha256:target456");
@@ -106,7 +110,8 @@ public sealed class DeltaCompareEndpointsTests
[Fact]
public async Task GetQuickDiff_MissingDigest_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/delta/quick?baseDigest=sha256:base123");
@@ -117,7 +122,8 @@ public sealed class DeltaCompareEndpointsTests
[Fact]
public async Task GetComparison_NotFound_ReturnsNotFound()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/delta/nonexistent-id");
@@ -128,7 +134,8 @@ public sealed class DeltaCompareEndpointsTests
[Fact]
public async Task PostCompare_DeterministicComparisonId_SameInputsSameId()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new DeltaCompareRequestDto

View File

@@ -17,14 +17,14 @@ namespace StellaOps.Scanner.WebService.Tests;
[Trait("Category", "Integration")]
[Trait("Sprint", "3410.0002")]
public sealed class EpssEndpointsTests : IDisposable
public sealed class EpssEndpointsTests : IAsyncLifetime
{
private readonly TestSurfaceSecretsScope _secrets;
private readonly InMemoryEpssProvider _epssProvider;
private readonly ScannerApplicationFactory _factory;
private readonly HttpClient _client;
private TestSurfaceSecretsScope _secrets = null!;
private InMemoryEpssProvider _epssProvider = null!;
private ScannerApplicationFactory _factory = null!;
private HttpClient _client = null!;
public EpssEndpointsTests()
public async Task InitializeAsync()
{
_secrets = new TestSurfaceSecretsScope();
_epssProvider = new InMemoryEpssProvider();
@@ -37,13 +37,14 @@ public sealed class EpssEndpointsTests : IDisposable
services.AddSingleton<IEpssProvider>(_epssProvider);
});
await _factory.InitializeAsync();
_client = _factory.CreateClient();
}
public void Dispose()
public async Task DisposeAsync()
{
_client.Dispose();
_factory.Dispose();
await _factory.DisposeAsync();
_secrets.Dispose();
}

View File

@@ -25,10 +25,11 @@ public sealed class EvidenceEndpointsTests
public async Task GetEvidence_ReturnsBadRequest_WhenScanIdInvalid()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
// Empty scan ID - route doesn't match
@@ -42,10 +43,11 @@ public sealed class EvidenceEndpointsTests
public async Task GetEvidence_ReturnsNotFound_WhenScanDoesNotExist()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync(
@@ -60,10 +62,11 @@ public sealed class EvidenceEndpointsTests
{
// When no finding ID is provided, the route matches the list endpoint
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
// Create a scan first
@@ -81,10 +84,11 @@ public sealed class EvidenceEndpointsTests
public async Task ListEvidence_ReturnsEmptyList_WhenNoFindings()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await CreateScanAsync(client);
@@ -106,10 +110,11 @@ public sealed class EvidenceEndpointsTests
// The current implementation returns empty list for non-existent scans
// because the reachability service returns empty findings for unknown scans
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/scans/nonexistent-scan/evidence");

View File

@@ -21,10 +21,11 @@ public sealed class FindingsEvidenceControllerTests
public async Task GetEvidence_ReturnsNotFound_WhenFindingMissing()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
await EnsureTriageSchemaAsync(factory);
using var client = factory.CreateClient();
@@ -38,10 +39,11 @@ public sealed class FindingsEvidenceControllerTests
public async Task GetEvidence_ReturnsForbidden_WhenRawScopeMissing()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
await EnsureTriageSchemaAsync(factory);
using var client = factory.CreateClient();
@@ -55,10 +57,11 @@ public sealed class FindingsEvidenceControllerTests
public async Task GetEvidence_ReturnsEvidence_WhenFindingExists()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
await EnsureTriageSchemaAsync(factory);
using var client = factory.CreateClient();
@@ -79,10 +82,11 @@ public sealed class FindingsEvidenceControllerTests
public async Task BatchEvidence_ReturnsBadRequest_WhenTooMany()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
await EnsureTriageSchemaAsync(factory);
using var client = factory.CreateClient();
@@ -101,10 +105,11 @@ public sealed class FindingsEvidenceControllerTests
public async Task BatchEvidence_ReturnsResults_ForExistingFindings()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
await EnsureTriageSchemaAsync(factory);
using var client = factory.CreateClient();

View File

@@ -10,7 +10,8 @@ public sealed class HealthEndpointsTests
[Fact]
public async Task HealthAndReadyEndpointsRespond()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var healthResponse = await client.GetAsync("/healthz");

View File

@@ -11,6 +11,7 @@ using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Determinism;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Services;
@@ -28,6 +29,7 @@ public sealed class HumanApprovalAttestationServiceTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly HumanApprovalAttestationService _service;
private readonly IGuidProvider _guidProvider = new SequentialGuidProvider();
public HumanApprovalAttestationServiceTests()
{
@@ -35,6 +37,7 @@ public sealed class HumanApprovalAttestationServiceTests
_service = new HumanApprovalAttestationService(
NullLogger<HumanApprovalAttestationService>.Instance,
MsOptions.Options.Create(new HumanApprovalAttestationOptions { DefaultApprovalTtlDays = 30 }),
_guidProvider,
_timeProvider);
}
@@ -319,7 +322,7 @@ public sealed class HumanApprovalAttestationServiceTests
public async Task GetAttestationAsync_NonExistentAttestation_ReturnsNull()
{
// Act
var result = await _service.GetAttestationAsync(ScanId.New(), "nonexistent");
var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), "nonexistent");
// Assert
result.Should().BeNull();
@@ -338,6 +341,7 @@ public sealed class HumanApprovalAttestationServiceTests
var service = new HumanApprovalAttestationService(
NullLogger<HumanApprovalAttestationService>.Instance,
MsOptions.Options.Create(new HumanApprovalAttestationOptions()),
_guidProvider,
expiredProvider);
// Need to create in this service instance for the store to be shared
@@ -354,7 +358,7 @@ public sealed class HumanApprovalAttestationServiceTests
await _service.CreateAttestationAsync(input);
// Act
var result = await _service.GetAttestationAsync(ScanId.New(), input.FindingId);
var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), input.FindingId);
// Assert
result.Should().BeNull();
@@ -384,7 +388,7 @@ public sealed class HumanApprovalAttestationServiceTests
public async Task GetApprovalsByScanAsync_MultipleApprovals_ReturnsAll()
{
// Arrange
var scanId = ScanId.New();
var scanId = ScanId.New(_guidProvider);
var input1 = CreateValidInput() with { ScanId = scanId, FindingId = "CVE-2024-0001" };
var input2 = CreateValidInput() with { ScanId = scanId, FindingId = "CVE-2024-0002" };
@@ -403,7 +407,7 @@ public sealed class HumanApprovalAttestationServiceTests
public async Task GetApprovalsByScanAsync_NoApprovals_ReturnsEmptyList()
{
// Act
var results = await _service.GetApprovalsByScanAsync(ScanId.New());
var results = await _service.GetApprovalsByScanAsync(ScanId.New(_guidProvider));
// Assert
results.Should().BeEmpty();
@@ -414,7 +418,7 @@ public sealed class HumanApprovalAttestationServiceTests
public async Task GetApprovalsByScanAsync_ExcludesRevokedApprovals()
{
// Arrange
var scanId = ScanId.New();
var scanId = ScanId.New(_guidProvider);
var input = CreateValidInput() with { ScanId = scanId };
await _service.CreateAttestationAsync(input);
await _service.RevokeApprovalAsync(scanId, input.FindingId, "admin", "Testing");
@@ -455,7 +459,7 @@ public sealed class HumanApprovalAttestationServiceTests
{
// Act
var result = await _service.RevokeApprovalAsync(
ScanId.New(),
ScanId.New(_guidProvider),
"nonexistent",
"admin@example.com",
"Testing");
@@ -541,7 +545,7 @@ public sealed class HumanApprovalAttestationServiceTests
{
return new HumanApprovalAttestationInput
{
ScanId = ScanId.New(),
ScanId = ScanId.New(_guidProvider),
FindingId = "CVE-2024-12345",
Decision = ApprovalDecision.AcceptRisk,
ApproverUserId = "security-lead@example.com",

View File

@@ -24,20 +24,25 @@ public sealed class IdempotencyMiddlewareTests
private const string IdempotencyKeyHeader = "X-Idempotency-Key";
private const string IdempotencyCachedHeader = "X-Idempotency-Cached";
private static ScannerApplicationFactory CreateFactory() =>
new ScannerApplicationFactory().WithOverrides(
private static async Task<ScannerApplicationFactory> CreateFactoryAsync()
{
var factory = new ScannerApplicationFactory().WithOverrides(
configureConfiguration: config =>
{
config["Scanner:Idempotency:Enabled"] = "true";
config["Scanner:Idempotency:Window"] = "24:00:00";
});
await factory.InitializeAsync();
return factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PostRequest_WithContentDigest_ReturnsIdempotencyKey()
{
// Arrange
await using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var content = new StringContent("""{"test":"data"}""", Encoding.UTF8, "application/json");
@@ -58,7 +63,7 @@ public sealed class IdempotencyMiddlewareTests
public async Task DuplicateRequest_WithSameContentDigest_ReturnsCachedResponse()
{
// Arrange
await using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var requestBody = """{"artifactDigest":"sha256:test123"}""";
@@ -84,7 +89,7 @@ public sealed class IdempotencyMiddlewareTests
public async Task DifferentRequests_WithDifferentDigests_AreProcessedSeparately()
{
// Arrange
await using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var requestBody1 = """{"artifactDigest":"sha256:unique1"}""";
@@ -110,7 +115,7 @@ public sealed class IdempotencyMiddlewareTests
public async Task GetRequest_BypassesIdempotencyMiddleware()
{
// Arrange
await using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
// Act
@@ -125,7 +130,7 @@ public sealed class IdempotencyMiddlewareTests
public async Task PostRequest_WithoutContentDigest_ComputesDigest()
{
// Arrange
await using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var content = new StringContent("""{"test":"nodigest"}""", Encoding.UTF8, "application/json");

View File

@@ -27,7 +27,7 @@ public sealed class EvidenceIntegrationTests : IAsyncLifetime
private ScannerApplicationFactory _factory = null!;
private HttpClient _client = null!;
public ValueTask InitializeAsync()
public async ValueTask InitializeAsync()
{
_factory = new ScannerApplicationFactory().WithOverrides(
configuration =>
@@ -41,8 +41,8 @@ public sealed class EvidenceIntegrationTests : IAsyncLifetime
services.AddSingleton<IArtifactObjectStore>(new InMemoryArtifactObjectStore());
});
await _factory.InitializeAsync();
_client = _factory.CreateClient();
return ValueTask.CompletedTask;
}
public async ValueTask DisposeAsync()

View File

@@ -29,7 +29,7 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime
private ScannerApplicationFactory _factory = null!;
private HttpClient _client = null!;
public ValueTask InitializeAsync()
public async ValueTask InitializeAsync()
{
_factory = new ScannerApplicationFactory().WithOverrides(
configuration =>
@@ -48,8 +48,8 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime
services.AddSingleton<IPedigreeDataProvider>(new MockPedigreeDataProvider());
});
await _factory.InitializeAsync();
_client = _factory.CreateClient();
return ValueTask.CompletedTask;
}
public async ValueTask DisposeAsync()
@@ -412,7 +412,7 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime
return Task.FromResult<PedigreeData?>(null);
}
public Task<IReadOnlyDictionary<string, PedigreeData>> GetPedigreesBatchAsync(
public async Task<IReadOnlyDictionary<string, PedigreeData>> GetPedigreesBatchAsync(
IEnumerable<string> purls,
CancellationToken cancellationToken = default)
{
@@ -420,14 +420,14 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime
foreach (var purl in purls)
{
var data = GetPedigreeAsync(purl, cancellationToken).Result;
var data = await GetPedigreeAsync(purl, cancellationToken);
if (data != null)
{
results[purl] = data;
}
}
return Task.FromResult<IReadOnlyDictionary<string, PedigreeData>>(results);
return results;
}
}

View File

@@ -2,7 +2,7 @@
// ProofReplayWorkflowTests.cs
// Sprint: SPRINT_3500_0002_0003_proof_replay_api
// Task: T7 - Integration Tests for Proof Replay Workflow
// Description: End-to-end tests for scan manifest proofs workflow
// Description: End-to-end tests for scan -> manifest -> proofs workflow
// -----------------------------------------------------------------------------
using System.Net;
@@ -20,7 +20,7 @@ namespace StellaOps.Scanner.WebService.Tests.Integration;
/// <summary>
/// Integration tests for the complete proof replay workflow:
/// Submit scan Get manifest Replay score Get proofs.
/// Submit scan -> Get manifest -> Replay score -> Get proofs.
/// </summary>
public sealed class ProofReplayWorkflowTests
{
@@ -31,27 +31,28 @@ public sealed class ProofReplayWorkflowTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var scope = factory.Services.CreateScope();
var manifestRepository = scope.ServiceProvider.GetRequiredService<IScanManifestRepository>();
var bundleRepository = scope.ServiceProvider.GetRequiredService<IProofBundleRepository>();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(1);
// Seed test data for the scan
var manifestRow = new ScanManifestRow
{
ManifestId = Guid.NewGuid(),
ManifestId = CreateGuid(2),
ScanId = scanId,
ManifestHash = "sha256:workflow-manifest",
SbomHash = "sha256:workflow-sbom",
RulesHash = "sha256:workflow-rules",
FeedHash = "sha256:workflow-feed",
PolicyHash = "sha256:workflow-policy",
ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-10),
ScanCompletedAt = DateTimeOffset.UtcNow,
ScanStartedAt = FixedNow.AddMinutes(-10),
ScanCompletedAt = FixedNow,
ManifestContent = """{"version":"1.0","test":"workflow"}""",
ScannerVersion = "1.0.0-integration",
CreatedAt = DateTimeOffset.UtcNow
CreatedAt = FixedNow
};
await manifestRepository.SaveAsync(manifestRow);
@@ -62,7 +63,7 @@ public sealed class ProofReplayWorkflowTests
RootHash = "sha256:workflow-root",
BundleType = "standard",
BundleHash = "sha256:workflow-bundle",
CreatedAt = DateTimeOffset.UtcNow
CreatedAt = FixedNow
};
await bundleRepository.SaveAsync(proofBundle);
@@ -103,11 +104,12 @@ public sealed class ProofReplayWorkflowTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var scope = factory.Services.CreateScope();
var manifestRepository = scope.ServiceProvider.GetRequiredService<IScanManifestRepository>();
var bundleRepository = scope.ServiceProvider.GetRequiredService<IProofBundleRepository>();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(3);
// Create two proof bundles with the same content should produce same hash
var manifestContent = """{"version":"1.0","inputs":{"deterministic":true,"seed":"test-seed-123"}}""";
@@ -115,18 +117,18 @@ public sealed class ProofReplayWorkflowTests
var manifestRow = new ScanManifestRow
{
ManifestId = Guid.NewGuid(),
ManifestId = CreateGuid(4),
ScanId = scanId,
ManifestHash = $"sha256:{expectedHash}",
SbomHash = "sha256:deterministic-sbom",
RulesHash = "sha256:deterministic-rules",
FeedHash = "sha256:deterministic-feed",
PolicyHash = "sha256:deterministic-policy",
ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
ScanCompletedAt = DateTimeOffset.UtcNow,
ScanStartedAt = FixedNow.AddMinutes(-5),
ScanCompletedAt = FixedNow,
ManifestContent = manifestContent,
ScannerVersion = "1.0.0-deterministic",
CreatedAt = DateTimeOffset.UtcNow
CreatedAt = FixedNow
};
await manifestRepository.SaveAsync(manifestRow);
@@ -161,6 +163,7 @@ public sealed class ProofReplayWorkflowTests
{
config["Scanner:Idempotency:Enabled"] = "true";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var requestBody = """{"artifactDigest":"sha256:idempotent-test-123"}""";
@@ -195,8 +198,9 @@ public sealed class ProofReplayWorkflowTests
config["scanner:rateLimiting:manifestPermitLimit"] = "2";
config["scanner:rateLimiting:manifestWindow"] = "00:00:30";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(5);
// Act - Send requests exceeding the limit
var responses = new List<HttpResponseMessage>();
@@ -226,8 +230,9 @@ public sealed class ProofReplayWorkflowTests
config["scanner:rateLimiting:manifestPermitLimit"] = "1";
config["scanner:rateLimiting:manifestWindow"] = "01:00:00";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(6);
// First request
await client.GetAsync($"/api/v1/scans/{scanId}/manifest");
@@ -246,6 +251,11 @@ public sealed class ProofReplayWorkflowTests
#endregion
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
private static Guid CreateGuid(int seed)
=> new($"00000000-0000-0000-0000-{seed:D12}");
#region Helper Methods
private static string ComputeSha256(string content)

View File

@@ -31,7 +31,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
private ScannerApplicationFactory _factory = null!;
private HttpClient _client = null!;
public ValueTask InitializeAsync()
public async ValueTask InitializeAsync()
{
_factory = new ScannerApplicationFactory().WithOverrides(
configuration =>
@@ -51,8 +51,8 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
services.AddSingleton<ISbomValidator>(new MockSbomValidator());
});
await _factory.InitializeAsync();
_client = _factory.CreateClient();
return ValueTask.CompletedTask;
}
public async ValueTask DisposeAsync()
@@ -159,7 +159,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
sbomBytes,
SbomFormat.CycloneDxJson,
validationOptions,
CancellationToken.None);
TestContext.Current.CancellationToken);
// Assert
Assert.NotNull(result);
@@ -189,7 +189,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
spdxBytes,
SbomFormat.Spdx3JsonLd,
validationOptions,
CancellationToken.None);
TestContext.Current.CancellationToken);
// Assert
Assert.NotNull(result);
@@ -207,7 +207,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
Assert.True(mockValidator.SupportsFormat(SbomFormat.Spdx3JsonLd));
Assert.True(mockValidator.SupportsFormat(SbomFormat.Unknown));
var info = await mockValidator.GetInfoAsync(CancellationToken.None);
var info = await mockValidator.GetInfoAsync(TestContext.Current.CancellationToken);
Assert.True(info.IsAvailable);
Assert.Contains(SbomFormat.CycloneDxJson, info.SupportedFormats);
}
@@ -229,7 +229,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
sbomBytes,
SbomFormat.CycloneDxJson,
validationOptions,
CancellationToken.None);
TestContext.Current.CancellationToken);
// Assert
Assert.NotNull(result);
@@ -254,7 +254,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
sbomBytes,
SbomFormat.CycloneDxJson,
validationOptions,
CancellationToken.None);
TestContext.Current.CancellationToken);
// Assert
Assert.NotNull(result);
@@ -310,11 +310,11 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime
Reference = "example.com/validation-test:1.0",
Digest = "sha256:validation123"
}
});
}, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<ScanSubmitResponse>();
var payload = await response.Content.ReadFromJsonAsync<ScanSubmitResponse>(TestContext.Current.CancellationToken);
Assert.NotNull(payload);
return payload!.ScanId;
}

View File

@@ -34,11 +34,12 @@ public sealed class LayerSbomEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
// Submit scan via HTTP POST to get scan ID
@@ -62,11 +63,12 @@ public sealed class LayerSbomEndpointsTests
public async Task ListLayers_WhenScanNotFound_Returns404()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/scan-not-found/layers");
@@ -81,11 +83,12 @@ public sealed class LayerSbomEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
@@ -121,11 +124,12 @@ public sealed class LayerSbomEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
@@ -149,11 +153,12 @@ public sealed class LayerSbomEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
@@ -177,11 +182,12 @@ public sealed class LayerSbomEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
@@ -201,11 +207,12 @@ public sealed class LayerSbomEndpointsTests
public async Task GetLayerSbom_WhenScanNotFound_Returns404()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/scan-not-found/layers/sha256:layer123/sbom");
@@ -220,11 +227,12 @@ public sealed class LayerSbomEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
@@ -246,11 +254,12 @@ public sealed class LayerSbomEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
@@ -273,11 +282,12 @@ public sealed class LayerSbomEndpointsTests
public async Task GetCompositionRecipe_WhenScanNotFound_Returns404()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/scan-not-found/composition-recipe");
@@ -292,11 +302,12 @@ public sealed class LayerSbomEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
@@ -326,12 +337,13 @@ public sealed class LayerSbomEndpointsTests
Errors = ImmutableArray<string>.Empty,
});
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<ILayerSbomService>();
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null);
@@ -359,12 +371,13 @@ public sealed class LayerSbomEndpointsTests
Errors = ImmutableArray.Create("Merkle root mismatch: expected sha256:abc, got sha256:def"),
});
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<ILayerSbomService>();
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null);
@@ -382,12 +395,13 @@ public sealed class LayerSbomEndpointsTests
[Fact]
public async Task VerifyCompositionRecipe_WhenScanNotFound_Returns404()
{
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<ILayerSbomService>();
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.PostAsync($"{BasePath}/scan-not-found/composition-recipe/verify", null);

View File

@@ -35,26 +35,27 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
using var scope = factory.Services.CreateScope();
var manifestRepository = scope.ServiceProvider.GetRequiredService<IScanManifestRepository>();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(1);
var manifestRow = new ScanManifestRow
{
ManifestId = Guid.NewGuid(),
ManifestId = CreateGuid(2),
ScanId = scanId,
ManifestHash = "sha256:manifest123",
SbomHash = "sha256:sbom123",
RulesHash = "sha256:rules123",
FeedHash = "sha256:feed123",
PolicyHash = "sha256:policy123",
ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
ScanCompletedAt = DateTimeOffset.UtcNow,
ScanStartedAt = FixedNow.AddMinutes(-5),
ScanCompletedAt = FixedNow,
ManifestContent = """{"version":"1.0","inputs":{"sbomHash":"sha256:sbom123"}}""",
ScannerVersion = "1.0.0-test",
CreatedAt = DateTimeOffset.UtcNow
CreatedAt = FixedNow
};
await manifestRepository.SaveAsync(manifestRow, TestContext.Current.CancellationToken);
@@ -82,8 +83,9 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(3);
// Act
var response = await client.GetAsync($"/api/v1/scans/{scanId}/manifest", TestContext.Current.CancellationToken);
@@ -98,6 +100,7 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
// Act
@@ -113,11 +116,12 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
using var scope = factory.Services.CreateScope();
var manifestRepository = scope.ServiceProvider.GetRequiredService<IScanManifestRepository>();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(4);
var manifestContent = JsonSerializer.Serialize(new
{
@@ -133,18 +137,18 @@ public sealed class ManifestEndpointsTests
var manifestRow = new ScanManifestRow
{
ManifestId = Guid.NewGuid(),
ManifestId = CreateGuid(5),
ScanId = scanId,
ManifestHash = "sha256:manifest456",
SbomHash = "sha256:sbom123",
RulesHash = "sha256:rules123",
FeedHash = "sha256:feed123",
PolicyHash = "sha256:policy123",
ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
ScanCompletedAt = DateTimeOffset.UtcNow,
ScanStartedAt = FixedNow.AddMinutes(-5),
ScanCompletedAt = FixedNow,
ManifestContent = manifestContent,
ScannerVersion = "1.0.0-test",
CreatedAt = DateTimeOffset.UtcNow
CreatedAt = FixedNow
};
await manifestRepository.SaveAsync(manifestRow, TestContext.Current.CancellationToken);
@@ -173,26 +177,27 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
using var scope = factory.Services.CreateScope();
var manifestRepository = scope.ServiceProvider.GetRequiredService<IScanManifestRepository>();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(6);
var manifestRow = new ScanManifestRow
{
ManifestId = Guid.NewGuid(),
ManifestId = CreateGuid(7),
ScanId = scanId,
ManifestHash = "sha256:content-digest-test",
SbomHash = "sha256:sbom789",
RulesHash = "sha256:rules789",
FeedHash = "sha256:feed789",
PolicyHash = "sha256:policy789",
ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-2),
ScanCompletedAt = DateTimeOffset.UtcNow,
ScanStartedAt = FixedNow.AddMinutes(-2),
ScanCompletedAt = FixedNow,
ManifestContent = """{"test":"content-digest"}""",
ScannerVersion = "1.0.0-test",
CreatedAt = DateTimeOffset.UtcNow
CreatedAt = FixedNow
};
await manifestRepository.SaveAsync(manifestRow, TestContext.Current.CancellationToken);
@@ -219,8 +224,9 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(8);
// Act
var response = await client.GetAsync($"/api/v1/scans/{scanId}/proofs");
@@ -240,11 +246,12 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
using var scope = factory.Services.CreateScope();
var bundleRepository = scope.ServiceProvider.GetRequiredService<IProofBundleRepository>();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(9);
var bundle1 = new ProofBundleRow
{
@@ -252,7 +259,7 @@ public sealed class ManifestEndpointsTests
RootHash = "sha256:root1",
BundleType = "standard",
BundleHash = "sha256:bundle1",
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5)
CreatedAt = FixedNow.AddMinutes(-5)
};
var bundle2 = new ProofBundleRow
@@ -261,7 +268,7 @@ public sealed class ManifestEndpointsTests
RootHash = "sha256:root2",
BundleType = "extended",
BundleHash = "sha256:bundle2",
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-2)
CreatedAt = FixedNow.AddMinutes(-2)
};
await bundleRepository.SaveAsync(bundle1);
@@ -286,6 +293,7 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
// Act
@@ -305,11 +313,12 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
using var scope = factory.Services.CreateScope();
var bundleRepository = scope.ServiceProvider.GetRequiredService<IProofBundleRepository>();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(10);
var rootHash = "sha256:detailroot1";
var bundle = new ProofBundleRow
@@ -324,8 +333,8 @@ public sealed class ManifestEndpointsTests
VexHash = "sha256:vex1",
SignatureKeyId = "key-001",
SignatureAlgorithm = "ed25519",
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-3),
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30)
CreatedAt = FixedNow.AddMinutes(-3),
ExpiresAt = FixedNow.AddDays(30)
};
await bundleRepository.SaveAsync(bundle);
@@ -356,8 +365,9 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(11);
// Act
var response = await client.GetAsync($"/api/v1/scans/{scanId}/proofs/sha256:nonexistent");
@@ -372,12 +382,13 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
using var scope = factory.Services.CreateScope();
var bundleRepository = scope.ServiceProvider.GetRequiredService<IProofBundleRepository>();
var scanId1 = Guid.NewGuid();
var scanId2 = Guid.NewGuid();
var scanId1 = CreateGuid(12);
var scanId2 = CreateGuid(13);
var rootHash = "sha256:crossscanroot";
var bundle = new ProofBundleRow
@@ -386,7 +397,7 @@ public sealed class ManifestEndpointsTests
RootHash = rootHash,
BundleType = "standard",
BundleHash = "sha256:crossscanbundle",
CreatedAt = DateTimeOffset.UtcNow
CreatedAt = FixedNow
};
await bundleRepository.SaveAsync(bundle);
@@ -404,6 +415,7 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
// Act
@@ -419,8 +431,9 @@ public sealed class ManifestEndpointsTests
{
// Arrange
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
var scanId = CreateGuid(14);
// Act - Trailing slash with empty root hash
var response = await client.GetAsync($"/api/v1/scans/{scanId}/proofs/");
@@ -431,4 +444,9 @@ public sealed class ManifestEndpointsTests
}
#endregion
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
private static Guid CreateGuid(int seed)
=> new($"00000000-0000-0000-0000-{seed:D12}");
}

View File

@@ -29,7 +29,7 @@ public sealed class OfflineKitEndpointsTests
var (keyId, keyPem, dsseJson) = CreateSignedDsse(bundleBytes);
File.WriteAllText(Path.Combine(trustRoots.Path, $"{keyId}.pem"), keyPem, Encoding.UTF8);
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
config["Scanner:OfflineKit:RequireDsse"] = "true";
@@ -39,6 +39,7 @@ public sealed class OfflineKitEndpointsTests
config["Scanner:OfflineKit:TrustAnchors:0:PurlPattern"] = "*";
config["Scanner:OfflineKit:TrustAnchors:0:AllowedKeyIds:0"] = keyId;
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -97,7 +98,7 @@ public sealed class OfflineKitEndpointsTests
signatures = new[] { new { keyid = keyId, sig = Convert.ToBase64String(new byte[] { 1, 2, 3 }) } }
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
config["Scanner:OfflineKit:RequireDsse"] = "true";
@@ -107,6 +108,7 @@ public sealed class OfflineKitEndpointsTests
config["Scanner:OfflineKit:TrustAnchors:0:PurlPattern"] = "*";
config["Scanner:OfflineKit:TrustAnchors:0:AllowedKeyIds:0"] = keyId;
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -151,12 +153,13 @@ public sealed class OfflineKitEndpointsTests
signatures = new[] { new { keyid = "unknown", sig = Convert.ToBase64String(new byte[] { 1, 2, 3 }) } }
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
config["Scanner:OfflineKit:RequireDsse"] = "false";
config["Scanner:OfflineKit:RekorOfflineMode"] = "false";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -192,7 +195,7 @@ public sealed class OfflineKitEndpointsTests
var auditEmitter = new CapturingAuditEmitter();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
config["Scanner:OfflineKit:RequireDsse"] = "false";
@@ -202,6 +205,7 @@ public sealed class OfflineKitEndpointsTests
services.RemoveAll<IOfflineKitAuditEmitter>();
services.AddSingleton<IOfflineKitAuditEmitter>(auditEmitter);
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -241,10 +245,11 @@ public sealed class OfflineKitEndpointsTests
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -262,11 +267,12 @@ public sealed class OfflineKitEndpointsTests
var bundleBytes = Encoding.UTF8.GetBytes("deterministic-offline-kit-bundle");
var bundleSha = ComputeSha256Hex(bundleBytes);
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
config["Scanner:OfflineKit:RequireDsse"] = "false";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -304,10 +310,11 @@ public sealed class OfflineKitEndpointsTests
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -346,10 +353,11 @@ public sealed class OfflineKitEndpointsTests
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -382,10 +390,11 @@ public sealed class OfflineKitEndpointsTests
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -425,10 +434,11 @@ public sealed class OfflineKitEndpointsTests
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -469,10 +479,11 @@ public sealed class OfflineKitEndpointsTests
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "false";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -498,10 +509,11 @@ public sealed class OfflineKitEndpointsTests
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -517,10 +529,11 @@ public sealed class OfflineKitEndpointsTests
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();
@@ -536,10 +549,11 @@ public sealed class OfflineKitEndpointsTests
{
using var contentRoot = new TempDirectory();
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
await using var factory = new ScannerApplicationFactory().WithOverrides(config =>
{
config["Scanner:OfflineKit:Enabled"] = "true";
});
await factory.InitializeAsync();
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
using var client = configured.CreateClient();

View File

@@ -13,11 +13,12 @@ public sealed class PlatformEventPublisherRegistrationTests
[Fact]
public void NullPublisherRegisteredWhenEventsDisabled()
{
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:events:enabled"] = "false";
configuration["scanner:events:dsn"] = string.Empty;
});
await factory.InitializeAsync();
using var scope = factory.Services.CreateScope();
var publisher = scope.ServiceProvider.GetRequiredService<IPlatformEventPublisher>();
@@ -44,7 +45,7 @@ public sealed class PlatformEventPublisherRegistrationTests
try
{
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:events:enabled"] = "true";
configuration["scanner:events:driver"] = "redis";
@@ -53,6 +54,7 @@ public sealed class PlatformEventPublisherRegistrationTests
configuration["scanner:events:publishTimeoutSeconds"] = "1";
configuration["scanner:events:maxStreamLength"] = "100";
});
await factory.InitializeAsync();
using var scope = factory.Services.CreateScope();
var options = scope.ServiceProvider.GetRequiredService<IOptions<ScannerWebServiceOptions>>().Value;

View File

@@ -11,6 +11,7 @@ using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Determinism;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Services;
@@ -28,6 +29,7 @@ public sealed class PolicyDecisionAttestationServiceTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly PolicyDecisionAttestationService _service;
private readonly IGuidProvider _guidProvider = new SequentialGuidProvider();
public PolicyDecisionAttestationServiceTests()
{
@@ -284,7 +286,7 @@ public sealed class PolicyDecisionAttestationServiceTests
{
// Act
var result = await _service.GetAttestationAsync(
ScanId.New(),
ScanId.New(_guidProvider),
"CVE-2024-00000@pkg:npm/nonexistent@1.0.0");
// Assert
@@ -301,7 +303,7 @@ public sealed class PolicyDecisionAttestationServiceTests
// Act
var result = await _service.GetAttestationAsync(
ScanId.New(), // Different scan ID
ScanId.New(_guidProvider), // Different scan ID
input.FindingId);
// Assert
@@ -393,7 +395,7 @@ public sealed class PolicyDecisionAttestationServiceTests
{
return new PolicyDecisionInput
{
ScanId = ScanId.New(),
ScanId = ScanId.New(_guidProvider),
FindingId = "CVE-2024-12345@pkg:npm/stripe@6.1.2",
Cve = "CVE-2024-12345",
ComponentPurl = "pkg:npm/stripe@6.1.2",

View File

@@ -18,7 +18,8 @@ public sealed class PolicyEndpointsTests
[Fact]
public async Task PolicySchemaReturnsEmbeddedSchema()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/policy/schema", TestContext.Current.CancellationToken);
@@ -34,7 +35,8 @@ public sealed class PolicyEndpointsTests
[Fact]
public async Task PolicyDiagnosticsReturnsRecommendations()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new PolicyDiagnosticsRequestDto
@@ -62,7 +64,8 @@ public sealed class PolicyEndpointsTests
[Fact]
public async Task PolicyPreviewUsesProposedPolicy()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
const string policyYaml = """

View File

@@ -22,6 +22,7 @@ public sealed class ProofSpineEndpointsTests
public async Task GetSpine_ReturnsSpine_WithVerification()
{
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
@@ -62,6 +63,7 @@ public sealed class ProofSpineEndpointsTests
public async Task GetSpine_ReturnsCbor_WhenAcceptHeaderRequestsCbor()
{
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
@@ -99,6 +101,7 @@ public sealed class ProofSpineEndpointsTests
public async Task ListSpinesByScan_ReturnsSummaries_WithSegmentCount()
{
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
@@ -138,6 +141,7 @@ public sealed class ProofSpineEndpointsTests
public async Task ListSpinesByScan_ReturnsCbor_WhenAcceptHeaderRequestsCbor()
{
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();
@@ -177,6 +181,7 @@ public sealed class ProofSpineEndpointsTests
public async Task GetSpine_ReturnsInvalidStatus_WhenSegmentTampered()
{
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var scope = factory.Services.CreateScope();
var builder = scope.ServiceProvider.GetRequiredService<ProofSpineBuilder>();

View File

@@ -22,8 +22,11 @@ public sealed class RateLimitingTests
private const string RateLimitRemainingHeader = "X-RateLimit-Remaining";
private const string RetryAfterHeader = "Retry-After";
private static ScannerApplicationFactory CreateFactory(int permitLimit = 100, int windowSeconds = 3600) =>
new ScannerApplicationFactory().WithOverrides(
private static async Task<ScannerApplicationFactory> CreateFactoryAsync(
int permitLimit = 100,
int windowSeconds = 3600)
{
var factory = new ScannerApplicationFactory().WithOverrides(
configureConfiguration: config =>
{
config["scanner:rateLimiting:scoreReplayPermitLimit"] = permitLimit.ToString();
@@ -34,12 +37,16 @@ public sealed class RateLimitingTests
config["scanner:rateLimiting:proofBundleWindow"] = TimeSpan.FromSeconds(windowSeconds).ToString();
});
await factory.InitializeAsync();
return factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ManifestEndpoint_IncludesRateLimitHeaders()
{
// Arrange
await using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
@@ -58,7 +65,7 @@ public sealed class RateLimitingTests
public async Task ProofBundleEndpoint_IncludesRateLimitHeaders()
{
// Arrange
await using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
@@ -76,7 +83,7 @@ public sealed class RateLimitingTests
public async Task ExcessiveRequests_Returns429()
{
// Arrange - Create factory with very low rate limit for testing
await using var factory = CreateFactory(permitLimit: 2, windowSeconds: 60);
await using var factory = await CreateFactoryAsync(permitLimit: 2, windowSeconds: 60);
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
@@ -103,7 +110,7 @@ public sealed class RateLimitingTests
public async Task RateLimited_Returns429WithRetryAfter()
{
// Arrange
await using var factory = CreateFactory(permitLimit: 1, windowSeconds: 3600);
await using var factory = await CreateFactoryAsync(permitLimit: 1, windowSeconds: 3600);
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
@@ -126,7 +133,7 @@ public sealed class RateLimitingTests
public async Task HealthEndpoint_NotRateLimited()
{
// Arrange
await using var factory = CreateFactory(permitLimit: 1);
await using var factory = await CreateFactoryAsync(permitLimit: 1);
using var client = factory.CreateClient();
// Act - Send multiple health requests
@@ -146,7 +153,7 @@ public sealed class RateLimitingTests
public async Task RateLimitedResponse_HasProblemDetails()
{
// Arrange
await using var factory = CreateFactory(permitLimit: 1, windowSeconds: 3600);
await using var factory = await CreateFactoryAsync(permitLimit: 1, windowSeconds: 3600);
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();
@@ -173,7 +180,7 @@ public sealed class RateLimitingTests
// In practice, this requires setting up different auth contexts
// Arrange
await using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var scanId = Guid.NewGuid();

View File

@@ -22,10 +22,11 @@ public sealed class ReachabilityDriftEndpointsTests
public async Task GetDriftReturnsNotFoundWhenNoResultAndNoBaseScanProvided()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -41,10 +42,11 @@ public sealed class ReachabilityDriftEndpointsTests
public async Task GetDriftComputesResultAndListsDriftedSinks()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();

View File

@@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Abstractions;
using StellaOps.Determinism;
using StellaOps.Policy;
using StellaOps.Scanner.Storage.Models;
using StellaOps.Scanner.Storage.Services;
@@ -36,7 +37,13 @@ public sealed class ReportEventDispatcherTests
{
var publisher = new RecordingEventPublisher();
var tracker = new RecordingClassificationChangeTracker();
var dispatcher = new ReportEventDispatcher(publisher, tracker, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
var dispatcher = new ReportEventDispatcher(
publisher,
tracker,
Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()),
new SequentialGuidProvider(),
TimeProvider.System,
NullLogger<ReportEventDispatcher>.Instance);
var cancellationToken = TestContext.Current.CancellationToken;
var request = new ReportRequestDto
@@ -177,7 +184,13 @@ public sealed class ReportEventDispatcherTests
{
var publisher = new RecordingEventPublisher();
var tracker = new RecordingClassificationChangeTracker();
var dispatcher = new ReportEventDispatcher(publisher, tracker, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
var dispatcher = new ReportEventDispatcher(
publisher,
tracker,
Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()),
new SequentialGuidProvider(),
TimeProvider.System,
NullLogger<ReportEventDispatcher>.Instance);
var cancellationToken = TestContext.Current.CancellationToken;
var request = new ReportRequestDto
@@ -262,7 +275,13 @@ public sealed class ReportEventDispatcherTests
{
ThrowOnTrack = true
};
var dispatcher = new ReportEventDispatcher(publisher, tracker, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
var dispatcher = new ReportEventDispatcher(
publisher,
tracker,
Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()),
new SequentialGuidProvider(),
TimeProvider.System,
NullLogger<ReportEventDispatcher>.Instance);
var cancellationToken = TestContext.Current.CancellationToken;
var request = new ReportRequestDto
@@ -333,7 +352,13 @@ public sealed class ReportEventDispatcherTests
var publisher = new RecordingEventPublisher();
var tracker = new RecordingClassificationChangeTracker();
var dispatcher = new ReportEventDispatcher(publisher, tracker, options, TimeProvider.System, NullLogger<ReportEventDispatcher>.Instance);
var dispatcher = new ReportEventDispatcher(
publisher,
tracker,
options,
new SequentialGuidProvider(),
TimeProvider.System,
NullLogger<ReportEventDispatcher>.Instance);
var cancellationToken = TestContext.Current.CancellationToken;
var request = new ReportRequestDto

View File

@@ -39,7 +39,7 @@ rules:
var hmacKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("scanner-report-hmac-key-2025!"));
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:signing:enabled"] = "true";
configuration["scanner:signing:keyId"] = "scanner-report-signing";
@@ -47,6 +47,7 @@ rules:
configuration["scanner:signing:keyPem"] = hmacKey;
configuration["scanner:features:enableSignedReports"] = "true";
});
await factory.InitializeAsync();
var store = factory.Services.GetRequiredService<PolicySnapshotStore>();
await store.SaveAsync(
@@ -110,7 +111,8 @@ rules:
[Fact]
public async Task ReportsEndpointValidatesDigest()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new ReportRequestDto
@@ -127,7 +129,8 @@ rules:
[Fact]
public async Task ReportsEndpointReturnsServiceUnavailableWhenPolicyMissing()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new ReportRequestDto
@@ -155,7 +158,7 @@ rules:
action: block
""";
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
configuration =>
{
configuration["scanner:signing:enabled"] = "true";
@@ -176,6 +179,7 @@ rules:
services.AddSingleton<RecordingPlatformEventPublisher>();
services.AddSingleton<IPlatformEventPublisher>(sp => sp.GetRequiredService<RecordingPlatformEventPublisher>());
});
await factory.InitializeAsync();
var store = factory.Services.GetRequiredService<PolicySnapshotStore>();
var saveResult = await store.SaveAsync(

View File

@@ -11,6 +11,7 @@ using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Determinism;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Services;
@@ -28,6 +29,7 @@ public sealed class RichGraphAttestationServiceTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly RichGraphAttestationService _service;
private readonly IGuidProvider _guidProvider = new SequentialGuidProvider();
public RichGraphAttestationServiceTests()
{
@@ -302,7 +304,7 @@ public sealed class RichGraphAttestationServiceTests
public async Task GetAttestationAsync_NonExistentAttestation_ReturnsNull()
{
// Act
var result = await _service.GetAttestationAsync(ScanId.New(), "nonexistent-graph");
var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), "nonexistent-graph");
// Assert
result.Should().BeNull();
@@ -317,7 +319,7 @@ public sealed class RichGraphAttestationServiceTests
await _service.CreateAttestationAsync(input);
// Act
var result = await _service.GetAttestationAsync(ScanId.New(), input.GraphId);
var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), input.GraphId);
// Assert
result.Should().BeNull();
@@ -396,7 +398,7 @@ public sealed class RichGraphAttestationServiceTests
{
return new RichGraphAttestationInput
{
ScanId = ScanId.New(),
ScanId = ScanId.New(_guidProvider),
GraphId = $"richgraph-{Guid.NewGuid():N}",
GraphDigest = "sha256:abc123def456789",
NodeCount = 1234,

View File

@@ -29,7 +29,8 @@ public sealed class RubyPackagesEndpointsTests
public async Task GetRubyPackagesReturnsNotFoundWhenInventoryMissing()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/scans/scan-ruby-missing/ruby-packages");
@@ -46,7 +47,8 @@ public sealed class RubyPackagesEndpointsTests
var generatedAt = DateTime.UtcNow.AddMinutes(-10);
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using (var serviceScope = factory.Services.CreateScope())
{
@@ -97,7 +99,8 @@ public sealed class RubyPackagesEndpointsTests
var generatedAt = DateTime.UtcNow.AddMinutes(-5);
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
string? scanId = null;
using (var scope = factory.Services.CreateScope())
@@ -155,7 +158,8 @@ public sealed class RubyPackagesEndpointsTests
const string reference = "ghcr.io/demo/ruby-service:latest";
const string digest = "sha512:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd";
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
string? scanId = null;
using (var scope = factory.Services.CreateScope())
@@ -248,10 +252,11 @@ public sealed class RubyPackagesEndpointsTests
new EntryTraceNdjsonMetadata("scan-placeholder", digest, generatedAt));
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
{
services.AddSingleton<IEntryTraceResultStore, RecordingEntryTraceResultStore>();
});
await factory.InitializeAsync();
string? canonicalScanId = null;
using (var scope = factory.Services.CreateScope())

View File

@@ -23,7 +23,8 @@ public sealed class RuntimeEndpointsTests
[Fact]
public async Task RuntimeEventsEndpointPersistsEvents()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new RuntimeEventsIngestRequestDto
@@ -62,7 +63,8 @@ public sealed class RuntimeEndpointsTests
[Fact]
public async Task RuntimeEventsEndpointRejectsUnsupportedSchema()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var envelope = CreateEnvelope("evt-100", schemaVersion: "zastava.runtime.event@v2.0");
@@ -80,13 +82,14 @@ public sealed class RuntimeEndpointsTests
[Fact]
public async Task RuntimeEventsEndpointEnforcesRateLimit()
{
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:runtime:perNodeBurst"] = "1";
configuration["scanner:runtime:perNodeEventsPerSecond"] = "1";
configuration["scanner:runtime:perTenantBurst"] = "1";
configuration["scanner:runtime:perTenantEventsPerSecond"] = "1";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new RuntimeEventsIngestRequestDto
@@ -112,10 +115,11 @@ public sealed class RuntimeEndpointsTests
[Fact]
public async Task RuntimePolicyEndpointReturnsDecisions()
{
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:runtime:policyCacheTtlSeconds"] = "600";
});
await factory.InitializeAsync();
const string imageDigest = "sha256:deadbeef";
@@ -170,20 +174,20 @@ rules:
await links.UpsertAsync(new LinkDocument
{
Id = Guid.NewGuid().ToString("N"),
Id = "link-0001",
FromType = LinkSourceType.Image,
FromDigest = imageDigest,
ArtifactId = sbomArtifactId,
CreatedAtUtc = DateTime.UtcNow
CreatedAtUtc = FixedUtc
}, TestContext.Current.CancellationToken);
await links.UpsertAsync(new LinkDocument
{
Id = Guid.NewGuid().ToString("N"),
Id = "link-0002",
FromType = LinkSourceType.Image,
FromDigest = imageDigest,
ArtifactId = attestationArtifactId,
CreatedAtUtc = DateTime.UtcNow
CreatedAtUtc = FixedUtc
}, TestContext.Current.CancellationToken);
}
@@ -195,7 +199,10 @@ rules:
CreateEnvelope("evt-211", imageDigest: imageDigest, buildId: "1122AABBCCDDEEFF00112233445566778899AABB")
}
};
var ingestResponse = await client.PostAsJsonAsync("/api/v1/runtime/events", ingestRequest);
var ingestResponse = await client.PostAsJsonAsync(
"/api/v1/runtime/events",
ingestRequest,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Accepted, ingestResponse.StatusCode);
var request = new RuntimePolicyRequestDto
@@ -205,7 +212,10 @@ rules:
Labels = new Dictionary<string, string> { ["app"] = "api" }
};
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request);
var response = await client.PostAsJsonAsync(
"/api/v1/policy/runtime",
request,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var raw = await response.Content.ReadAsStringAsync();
@@ -214,7 +224,7 @@ rules:
Assert.True(payload is not null, $"Runtime policy response: {raw}");
Assert.Equal(600, payload!.TtlSeconds);
Assert.NotNull(payload.PolicyRevision);
Assert.True(payload.ExpiresAtUtc > DateTimeOffset.UtcNow);
Assert.True(payload.ExpiresAtUtc > FixedNow);
var decision = payload.Results[imageDigest];
Assert.Equal("pass", decision.PolicyVerdict);
@@ -232,7 +242,6 @@ rules:
Assert.NotNull(decision.BuildIds);
Assert.Contains("1122aabbccddeeff00112233445566778899aabb", decision.BuildIds!);
var metadataString = decision.Metadata;
Console.WriteLine($"Runtime policy metadata: {metadataString ?? "<null>"}");
Assert.False(string.IsNullOrWhiteSpace(metadataString));
using var metadataDocument = JsonDocument.Parse(decision.Metadata!);
Assert.True(metadataDocument.RootElement.TryGetProperty("heuristics", out _));
@@ -242,7 +251,8 @@ rules:
[Fact]
public async Task RuntimePolicyEndpointFlagsUnsignedAndMissingSbom()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
const string imageDigest = "sha256:feedface";
@@ -268,10 +278,10 @@ rules: []
{
Namespace = "payments",
Images = new[] { imageDigest }
});
}, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<RuntimePolicyResponseDto>();
var payload = await response.Content.ReadFromJsonAsync<RuntimePolicyResponseDto>(TestContext.Current.CancellationToken);
Assert.NotNull(payload);
var decision = payload!.Results[imageDigest];
@@ -299,7 +309,8 @@ rules: []
[Fact]
public async Task RuntimePolicyEndpointValidatesRequest()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new RuntimePolicyRequestDto
@@ -307,7 +318,7 @@ rules: []
Images = Array.Empty<string>()
};
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request);
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
@@ -321,7 +332,7 @@ rules: []
var runtimeEvent = new RuntimeEvent
{
EventId = eventId,
When = DateTimeOffset.UtcNow,
When = FixedNow,
Kind = RuntimeEventKind.ContainerStart,
Tenant = "tenant-alpha",
Node = "node-a",
@@ -363,4 +374,7 @@ rules: []
Event = runtimeEvent
};
}
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
private static readonly DateTime FixedUtc = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
}

View File

@@ -30,7 +30,8 @@ public sealed class RuntimeReconciliationTests
[Fact]
public async Task ReconcileEndpoint_WithNoRuntimeEvents_ReturnsNotFound()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new RuntimeReconcileRequestDto
@@ -54,12 +55,13 @@ public sealed class RuntimeReconciliationTests
{
var mockObjectStore = new InMemoryArtifactObjectStore();
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
configureServices: services =>
{
services.RemoveAll<IArtifactObjectStore>();
services.AddSingleton<IArtifactObjectStore>(mockObjectStore);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
// Ingest runtime event with loaded libraries
@@ -104,12 +106,13 @@ public sealed class RuntimeReconciliationTests
{
var mockObjectStore = new InMemoryArtifactObjectStore();
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
configureServices: services =>
{
services.RemoveAll<IArtifactObjectStore>();
services.AddSingleton<IArtifactObjectStore>(mockObjectStore);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
// Setup: Create SBOM artifact with components
@@ -195,12 +198,13 @@ public sealed class RuntimeReconciliationTests
{
var mockObjectStore = new InMemoryArtifactObjectStore();
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
configureServices: services =>
{
services.RemoveAll<IArtifactObjectStore>();
services.AddSingleton<IArtifactObjectStore>(mockObjectStore);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
const string imageDigest = "sha256:pathtest123";
@@ -281,12 +285,13 @@ public sealed class RuntimeReconciliationTests
{
var mockObjectStore = new InMemoryArtifactObjectStore();
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
configureServices: services =>
{
services.RemoveAll<IArtifactObjectStore>();
services.AddSingleton<IArtifactObjectStore>(mockObjectStore);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
const string imageDigest = "sha256:eventidtest";
@@ -368,7 +373,8 @@ public sealed class RuntimeReconciliationTests
[Fact]
public async Task ReconcileEndpoint_WithNonExistentEventId_ReturnsNotFound()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new RuntimeReconcileRequestDto
@@ -390,7 +396,8 @@ public sealed class RuntimeReconciliationTests
[Fact]
public async Task ReconcileEndpoint_WithMissingImageDigest_ReturnsBadRequest()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new RuntimeReconcileRequestDto
@@ -409,12 +416,13 @@ public sealed class RuntimeReconciliationTests
{
var mockObjectStore = new InMemoryArtifactObjectStore();
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
configureServices: services =>
{
services.RemoveAll<IArtifactObjectStore>();
services.AddSingleton<IArtifactObjectStore>(mockObjectStore);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
const string imageDigest = "sha256:mixedtest";

View File

@@ -19,7 +19,7 @@ public sealed class SbomEndpointsTests
public async Task SubmitSbomAcceptsCycloneDxJson()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
}, configureServices: services =>
@@ -27,6 +27,7 @@ public sealed class SbomEndpointsTests
services.RemoveAll<IArtifactObjectStore>();
services.AddSingleton<IArtifactObjectStore>(new InMemoryArtifactObjectStore());
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await CreateScanAsync(client);

View File

@@ -18,7 +18,7 @@ public sealed class SbomUploadEndpointsTests
public async Task Upload_accepts_cyclonedx_fixture_and_returns_record()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var request = new SbomUploadRequestDto
@@ -64,7 +64,7 @@ public sealed class SbomUploadEndpointsTests
public async Task Upload_accepts_spdx_fixture_and_reports_quality_score()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var request = new SbomUploadRequestDto
@@ -90,7 +90,7 @@ public sealed class SbomUploadEndpointsTests
public async Task Upload_rejects_unknown_format()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = CreateFactory();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var invalid = new SbomUploadRequestDto
@@ -103,9 +103,9 @@ public sealed class SbomUploadEndpointsTests
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
private static ScannerApplicationFactory CreateFactory()
private static async Task<ScannerApplicationFactory> CreateFactoryAsync()
{
return new ScannerApplicationFactory().WithOverrides(configuration =>
var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
}, configureServices: services =>
@@ -113,6 +113,9 @@ public sealed class SbomUploadEndpointsTests
services.RemoveAll<IArtifactObjectStore>();
services.AddSingleton<IArtifactObjectStore>(new InMemoryArtifactObjectStore());
});
await factory.InitializeAsync();
return factory;
}
private static string LoadFixtureBase64(string fileName)

View File

@@ -21,12 +21,12 @@ using StellaOps.Scanner.Triage;
using StellaOps.Determinism;
using StellaOps.Scanner.WebService.Diagnostics;
using StellaOps.Scanner.WebService.Services;
using Xunit;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceStatus>
public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceStatus>, IAsyncLifetime, IAsyncDisposable
{
private readonly ScannerWebServicePostgresFixture? postgresFixture;
private readonly bool skipPostgres;
private readonly Dictionary<string, string?> configuration = new(StringComparer.OrdinalIgnoreCase)
{
@@ -53,6 +53,10 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
private Action<IDictionary<string, string?>>? configureConfiguration;
private Action<IServiceCollection>? configureServices;
private bool useTestAuthentication;
private ScannerWebServicePostgresFixture? postgresFixture;
private Task? initializationTask;
private bool initialized;
private bool disposed;
public ScannerApplicationFactory() : this(skipPostgres: false)
{
@@ -61,25 +65,16 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
private ScannerApplicationFactory(bool skipPostgres)
{
this.skipPostgres = skipPostgres;
initialized = skipPostgres;
if (!skipPostgres)
{
postgresFixture = new ScannerWebServicePostgresFixture();
postgresFixture.InitializeAsync().GetAwaiter().GetResult();
return;
}
var connectionBuilder = new NpgsqlConnectionStringBuilder(postgresFixture.ConnectionString)
{
SearchPath = $"{postgresFixture.SchemaName},public"
};
configuration["scanner:storage:dsn"] = connectionBuilder.ToString();
configuration["scanner:storage:database"] = postgresFixture.SchemaName;
}
else
{
// Lightweight mode: use stub connection string
configuration["scanner:storage:dsn"] = "Host=localhost;Database=test;";
configuration["scanner:storage:database"] = "test";
}
// Lightweight mode: use stub connection string
configuration["scanner:storage:dsn"] = "Host=localhost;Database=test;";
configuration["scanner:storage:database"] = "test";
}
/// <summary>
@@ -109,8 +104,62 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
return this;
}
public Task InitializeAsync()
{
initializationTask ??= InitializeCoreAsync();
return initializationTask;
}
private async Task InitializeCoreAsync()
{
if (initialized)
{
return;
}
if (skipPostgres)
{
initialized = true;
return;
}
postgresFixture = new ScannerWebServicePostgresFixture();
await postgresFixture.InitializeAsync();
var connectionBuilder = new NpgsqlConnectionStringBuilder(postgresFixture.ConnectionString)
{
SearchPath = $"{postgresFixture.SchemaName},public"
};
configuration["scanner:storage:dsn"] = connectionBuilder.ToString();
configuration["scanner:storage:database"] = postgresFixture.SchemaName;
initialized = true;
}
Task IAsyncLifetime.DisposeAsync() => DisposeAsync().AsTask();
public async ValueTask DisposeAsync()
{
if (disposed)
{
return;
}
disposed = true;
base.Dispose();
if (postgresFixture is not null)
{
await postgresFixture.DisposeAsync();
}
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
if (!initialized)
{
throw new InvalidOperationException("ScannerApplicationFactory must be initialized via InitializeAsync before use.");
}
configureConfiguration?.Invoke(configuration);
builder.UseEnvironment("Testing");
@@ -200,16 +249,6 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory<ServiceSta
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing && postgresFixture is not null)
{
postgresFixture.DisposeAsync().GetAwaiter().GetResult();
}
}
private sealed class TestSurfaceValidatorRunner : ISurfaceValidatorRunner
{
public ValueTask<SurfaceValidationResult> RunAllAsync(

View File

@@ -1,10 +1,11 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Xunit;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class ScannerApplicationFixture : IDisposable
public sealed class ScannerApplicationFixture : IAsyncLifetime
{
private ScannerApplicationFactory? _authenticatedFactory;
@@ -22,10 +23,12 @@ public sealed class ScannerApplicationFixture : IDisposable
return client;
}
public void Dispose()
public Task InitializeAsync() => Factory.InitializeAsync();
public async Task DisposeAsync()
{
_authenticatedFactory?.Dispose();
Factory.Dispose();
_authenticatedFactory = null;
await Factory.DisposeAsync();
}
}

View File

@@ -177,6 +177,16 @@ public sealed class ScannerSurfaceSecretConfiguratorTests
_handles = handles ?? throw new ArgumentNullException(nameof(handles));
}
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
{
if (_handles.TryGetValue(request.SecretType, out var handle))
{
return handle;
}
throw new SurfaceSecretNotFoundException(request);
}
public ValueTask<SurfaceSecretHandle> GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default)
{
if (_handles.TryGetValue(request.SecretType, out var handle))

View File

@@ -16,11 +16,12 @@ public sealed partial class ScansEndpointsTests
public async Task EntropyEndpoint_AttachesSnapshot_AndSurfacesInStatus()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(cfg =>
await using var factory = new ScannerApplicationFactory().WithOverrides(cfg =>
{
cfg["scanner:authority:enabled"] = "false";
cfg["scanner:authority:allowAnonymousFallback"] = "true";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();

View File

@@ -27,7 +27,7 @@ public sealed partial class ScansEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
var store = new InMemoryArtifactObjectStore();
using var factory = new ScannerApplicationFactory().WithOverrides(configureConfiguration: cfg =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configureConfiguration: cfg =>
{
cfg["scanner:artifactStore:bucket"] = "replay-bucket";
},
@@ -36,6 +36,7 @@ public sealed partial class ScansEndpointsTests
services.RemoveAll<IArtifactObjectStore>();
services.AddSingleton<IArtifactObjectStore>(store);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var submit = await client.PostAsJsonAsync("/api/v1/scans", new { image = new { digest = "sha256:demo" } }, TestContext.Current.CancellationToken);

View File

@@ -21,10 +21,11 @@ public sealed partial class ScansEndpointsTests
public async Task RecordModeService_AttachesReplayAndSurfacedInStatus()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(cfg =>
await using var factory = new ScannerApplicationFactory().WithOverrides(cfg =>
{
cfg["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var submitResponse = await client.PostAsJsonAsync("/api/v1/scans", new

View File

@@ -25,7 +25,8 @@ public sealed partial class ScansEndpointsTests
public async Task SubmitScanValidatesImageDescriptor()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/scans", new
@@ -43,7 +44,7 @@ public sealed partial class ScansEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
RecordingCoordinator coordinator = null!;
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
}, configureServices: services =>
@@ -57,6 +58,7 @@ public sealed partial class ScansEndpointsTests
return coordinator;
});
});
await factory.InitializeAsync();
using var client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
@@ -83,7 +85,7 @@ public sealed partial class ScansEndpointsTests
using var secrets = new TestSurfaceSecretsScope();
RecordingCoordinator coordinator = null!;
using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:determinism:feedSnapshotId"] = "feed-2025-11-26";
configuration["scanner:determinism:policySnapshotId"] = "rev-42";
@@ -98,6 +100,7 @@ public sealed partial class ScansEndpointsTests
return coordinator;
});
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new ScanSubmitRequest
@@ -155,10 +158,11 @@ public sealed partial class ScansEndpointsTests
var ndjson = EntryTraceNdjsonWriter.Serialize(graph, new EntryTraceNdjsonMetadata(scanId, "sha256:test", generatedAt));
var storedResult = new EntryTraceResult(scanId, "sha256:test", generatedAt, graph, ndjson);
using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
{
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(storedResult));
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
@@ -176,10 +180,11 @@ public sealed partial class ScansEndpointsTests
public async Task GetEntryTraceReturnsNotFoundWhenMissing()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
await using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services =>
{
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(null));
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/scans/scan-missing/entrytrace");

View File

@@ -19,13 +19,13 @@ namespace StellaOps.Scanner.WebService.Tests;
/// </summary>
[Trait("Category", "Integration")]
[Trait("Sprint", "3401.0002")]
public sealed class ScoreReplayEndpointsTests : IDisposable
public sealed class ScoreReplayEndpointsTests : IAsyncLifetime
{
private readonly TestSurfaceSecretsScope _secrets;
private readonly ScannerApplicationFactory _factory;
private readonly HttpClient _client;
private TestSurfaceSecretsScope _secrets = null!;
private ScannerApplicationFactory _factory = null!;
private HttpClient _client = null!;
public ScoreReplayEndpointsTests()
public async Task InitializeAsync()
{
_secrets = new TestSurfaceSecretsScope();
_factory = new ScannerApplicationFactory().WithOverrides(cfg =>
@@ -33,13 +33,14 @@ public sealed class ScoreReplayEndpointsTests : IDisposable
cfg["scanner:authority:enabled"] = "false";
cfg["scanner:scoreReplay:enabled"] = "true";
});
await _factory.InitializeAsync();
_client = _factory.CreateClient();
}
public void Dispose()
public async Task DisposeAsync()
{
_client.Dispose();
_factory.Dispose();
await _factory.DisposeAsync();
_secrets.Dispose();
}

View File

@@ -33,8 +33,9 @@ public sealed class ScannerAuthorizationTests
[InlineData("/api/v1/sbom/upload")]
public async Task ProtectedPostEndpoints_RequireAuthentication(string endpoint)
{
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
useTestAuthentication: true);
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -59,7 +60,8 @@ public sealed class ScannerAuthorizationTests
[InlineData("/readyz")]
public async Task HealthEndpoints_ArePubliclyAccessible(string endpoint)
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync(endpoint, TestContext.Current.CancellationToken);
@@ -81,8 +83,9 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task ExpiredToken_IsRejected()
{
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
useTestAuthentication: true);
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -111,8 +114,9 @@ public sealed class ScannerAuthorizationTests
[InlineData("Bearer only-one-part")]
public async Task MalformedToken_IsRejected(string token)
{
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
useTestAuthentication: true);
await factory.InitializeAsync();
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
@@ -134,8 +138,9 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task TokenWithWrongIssuer_IsRejected()
{
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
useTestAuthentication: true);
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -160,8 +165,9 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task TokenWithWrongAudience_IsRejected()
{
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
useTestAuthentication: true);
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -190,7 +196,8 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task AnonymousFallback_AllowsAccess_WhenNoAuthConfigured()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/healthz", TestContext.Current.CancellationToken);
@@ -208,8 +215,9 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task AnonymousFallback_DeniesAccess_WhenAuthRequired()
{
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
useTestAuthentication: true);
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -234,8 +242,9 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task WriteOperations_RequireAuthentication()
{
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
useTestAuthentication: true);
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -256,8 +265,9 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task DeleteOperations_RequireAuthentication()
{
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
useTestAuthentication: true);
await factory.InitializeAsync();
using var client = factory.CreateClient();
@@ -281,7 +291,8 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task RequestWithoutTenant_IsHandledAppropriately()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
// Request without tenant header - use health endpoint
@@ -305,7 +316,8 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task Responses_ContainSecurityHeaders()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/healthz", TestContext.Current.CancellationToken);
@@ -321,7 +333,8 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task Cors_IsProperlyConfigured()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Options, "/healthz");
@@ -349,8 +362,9 @@ public sealed class ScannerAuthorizationTests
[Fact]
public async Task ValidToken_IsAccepted()
{
using var factory = new ScannerApplicationFactory().WithOverrides(
await using var factory = new ScannerApplicationFactory().WithOverrides(
useTestAuthentication: true);
await factory.InitializeAsync();
using var client = factory.CreateClient();

View File

@@ -17,7 +17,7 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests
[Fact]
public void Configure_UsesSurfaceEnvironmentAndCacheRoot()
{
var cacheRoot = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
var cacheRoot = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "stellaops-tests", "surface-manifest"));
var settings = new SurfaceEnvironmentSettings(
new Uri("https://surface.example"),
"surface-bucket",
@@ -30,7 +30,7 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests
"tenant-a",
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()))
{
CreatedAtUtc = DateTimeOffset.UtcNow
CreatedAtUtc = FixedNow
};
var environment = new StubSurfaceEnvironment(settings);
@@ -45,6 +45,8 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests
Assert.Equal(Path.Combine(cacheRoot.FullName, "manifests"), options.RootDirectory);
}
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
private sealed class StubSurfaceEnvironment : ISurfaceEnvironment
{
public StubSurfaceEnvironment(SurfaceEnvironmentSettings settings)

View File

@@ -24,7 +24,8 @@ public sealed class TriageStatusEndpointsTests
[Fact]
public async Task GetFindingStatus_NotFound_ReturnsNotFound()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/triage/findings/nonexistent-finding");
@@ -35,7 +36,8 @@ public sealed class TriageStatusEndpointsTests
[Fact]
public async Task PostUpdateStatus_ValidRequest_ReturnsUpdatedStatus()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new UpdateTriageStatusRequestDto
@@ -54,7 +56,8 @@ public sealed class TriageStatusEndpointsTests
[Fact]
public async Task PostVexStatement_ValidRequest_ReturnsResponse()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new SubmitVexStatementRequestDto
@@ -73,7 +76,8 @@ public sealed class TriageStatusEndpointsTests
[Fact]
public async Task PostQuery_EmptyFilters_ReturnsResults()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new BulkTriageQueryRequestDto
@@ -94,7 +98,8 @@ public sealed class TriageStatusEndpointsTests
[Fact]
public async Task PostQuery_WithLaneFilter_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new BulkTriageQueryRequestDto
@@ -114,7 +119,8 @@ public sealed class TriageStatusEndpointsTests
[Fact]
public async Task PostQuery_WithVerdictFilter_FiltersCorrectly()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new BulkTriageQueryRequestDto
@@ -134,7 +140,8 @@ public sealed class TriageStatusEndpointsTests
[Fact]
public async Task GetSummary_ValidDigest_ReturnsSummary()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123");
@@ -150,7 +157,8 @@ public sealed class TriageStatusEndpointsTests
[Fact]
public async Task GetSummary_IncludesAllLanes()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123");
@@ -168,7 +176,8 @@ public sealed class TriageStatusEndpointsTests
[Fact]
public async Task GetSummary_IncludesAllVerdicts()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123");
@@ -186,7 +195,8 @@ public sealed class TriageStatusEndpointsTests
[Fact]
public async Task PostQuery_ResponseIncludesSummary()
{
using var factory = new ScannerApplicationFactory();
await using var factory = new ScannerApplicationFactory();
await factory.InitializeAsync();
using var client = factory.CreateClient();
var request = new BulkTriageQueryRequestDto

View File

@@ -185,7 +185,7 @@ public sealed class UnifiedEvidenceServiceTests
DeltaId = "delta-101",
PreviousScanId = "scan-099",
CurrentScanId = "scan-100",
ComparedAt = DateTimeOffset.UtcNow,
ComparedAt = FixedNow,
Summary = new DeltaSummaryDto
{
AddedCount = 5,
@@ -306,7 +306,7 @@ public sealed class UnifiedEvidenceServiceTests
ComponentPurl = "pkg:npm/test@1.0.0",
Manifests = CreateMinimalManifests(),
Verification = CreateMinimalVerification(),
GeneratedAt = DateTimeOffset.UtcNow,
GeneratedAt = FixedNow,
// All tabs null
Sbom = null,
Reachability = null,
@@ -346,7 +346,7 @@ public sealed class UnifiedEvidenceServiceTests
Status = "affected",
TrustScore = 1.0,
MeetsPolicyThreshold = true,
IssuedAt = DateTimeOffset.UtcNow
IssuedAt = FixedNow
},
new VexClaimDto
{
@@ -355,7 +355,7 @@ public sealed class UnifiedEvidenceServiceTests
Status = "not_affected",
TrustScore = 0.95,
MeetsPolicyThreshold = true,
IssuedAt = DateTimeOffset.UtcNow.AddDays(-1)
IssuedAt = FixedNow.AddDays(-1)
},
new VexClaimDto
{
@@ -364,12 +364,12 @@ public sealed class UnifiedEvidenceServiceTests
Status = "under_investigation",
TrustScore = 0.6,
MeetsPolicyThreshold = false,
IssuedAt = DateTimeOffset.UtcNow.AddDays(-7)
IssuedAt = FixedNow.AddDays(-7)
}
},
Manifests = CreateMinimalManifests(),
Verification = CreateMinimalVerification(),
GeneratedAt = DateTimeOffset.UtcNow
GeneratedAt = FixedNow
};
// Assert
@@ -429,7 +429,7 @@ public sealed class UnifiedEvidenceServiceTests
EvidenceBundleUrl = "https://api.stellaops.local/bundles/bundle-123",
Manifests = CreateMinimalManifests(),
Verification = CreateMinimalVerification(),
GeneratedAt = DateTimeOffset.UtcNow
GeneratedAt = FixedNow
};
// Assert
@@ -455,7 +455,7 @@ public sealed class UnifiedEvidenceServiceTests
AttestationsVerified = true,
EvidenceComplete = true,
Issues = null,
VerifiedAt = DateTimeOffset.UtcNow
VerifiedAt = FixedNow
};
// Assert
@@ -478,7 +478,7 @@ public sealed class UnifiedEvidenceServiceTests
AttestationsVerified = false,
EvidenceComplete = true,
Issues = new[] { "Attestation signature verification failed" },
VerifiedAt = DateTimeOffset.UtcNow
VerifiedAt = FixedNow
};
// Assert
@@ -505,7 +505,7 @@ public sealed class UnifiedEvidenceServiceTests
"Attestation not found",
"VEX evidence missing"
},
VerifiedAt = DateTimeOffset.UtcNow
VerifiedAt = FixedNow
};
// Assert
@@ -747,7 +747,7 @@ public sealed class UnifiedEvidenceServiceTests
EvidenceBundleUrl = "https://api.stellaops.local/bundles/scan-001-finding-001",
Manifests = CreateMinimalManifests(),
Verification = CreateMinimalVerification(),
GeneratedAt = DateTimeOffset.UtcNow
GeneratedAt = FixedNow
};
// Assert
@@ -809,7 +809,7 @@ public sealed class UnifiedEvidenceServiceTests
Status = "not_affected",
TrustScore = 0.95,
MeetsPolicyThreshold = true,
IssuedAt = DateTimeOffset.UtcNow
IssuedAt = FixedNow
}
},
Attestations = new[]
@@ -827,7 +827,7 @@ public sealed class UnifiedEvidenceServiceTests
DeltaId = "delta-001",
PreviousScanId = "scan-099",
CurrentScanId = "scan-100",
ComparedAt = DateTimeOffset.UtcNow
ComparedAt = FixedNow
},
Policy = new PolicyEvidenceDto
{
@@ -838,7 +838,7 @@ public sealed class UnifiedEvidenceServiceTests
Manifests = CreateMinimalManifests(),
Verification = CreateMinimalVerification(),
ReplayCommand = "stellaops replay --target pkg:npm/lodash@4.17.21 --verify",
GeneratedAt = DateTimeOffset.UtcNow
GeneratedAt = FixedNow
};
private static string DetermineVerificationStatus(
@@ -852,6 +852,8 @@ public sealed class UnifiedEvidenceServiceTests
}
#endregion
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
}
/// <summary>

View File

@@ -24,12 +24,13 @@ public sealed class VexGateEndpointsTests
[Fact]
public async Task GetGatePolicy_ReturnsPolicy()
{
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/gate-policy");
@@ -44,12 +45,13 @@ public sealed class VexGateEndpointsTests
[Fact]
public async Task GetGatePolicy_WithTenantId_ReturnsPolicy()
{
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/gate-policy?tenantId=tenant-a");
@@ -62,12 +64,13 @@ public sealed class VexGateEndpointsTests
[Fact]
public async Task GetGateResults_WhenScanNotFound_Returns404()
{
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/scan-not-exists/gate-results");
@@ -78,16 +81,17 @@ public sealed class VexGateEndpointsTests
[Fact]
public async Task GetGateResults_WhenScanExists_ReturnsResults()
{
var scanId = "scan-" + Guid.NewGuid().ToString("N");
var scanId = "scan-0001";
var mockService = new InMemoryVexGateQueryService();
mockService.AddScanResult(scanId, CreateTestGateResults(scanId));
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/{scanId}/gate-results");
@@ -103,16 +107,17 @@ public sealed class VexGateEndpointsTests
[Fact]
public async Task GetGateResults_WithDecisionFilter_ReturnsFilteredResults()
{
var scanId = "scan-" + Guid.NewGuid().ToString("N");
var scanId = "scan-0002";
var mockService = new InMemoryVexGateQueryService();
mockService.AddScanResult(scanId, CreateTestGateResults(scanId, blockedCount: 3, warnCount: 5, passCount: 10));
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/{scanId}/gate-results?decision=Block");
@@ -126,12 +131,13 @@ public sealed class VexGateEndpointsTests
[Fact]
public async Task GetGateSummary_WhenScanNotFound_Returns404()
{
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/scan-not-exists/gate-summary");
@@ -142,16 +148,17 @@ public sealed class VexGateEndpointsTests
[Fact]
public async Task GetGateSummary_WhenScanExists_ReturnsSummary()
{
var scanId = "scan-" + Guid.NewGuid().ToString("N");
var scanId = "scan-0003";
var mockService = new InMemoryVexGateQueryService();
mockService.AddScanResult(scanId, CreateTestGateResults(scanId, blockedCount: 2, warnCount: 8, passCount: 40));
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/{scanId}/gate-summary");
@@ -168,12 +175,13 @@ public sealed class VexGateEndpointsTests
[Fact]
public async Task GetBlockedFindings_WhenScanNotFound_Returns404()
{
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService, InMemoryVexGateQueryService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/scan-not-exists/gate-blocked");
@@ -184,16 +192,17 @@ public sealed class VexGateEndpointsTests
[Fact]
public async Task GetBlockedFindings_WhenScanExists_ReturnsOnlyBlocked()
{
var scanId = "scan-" + Guid.NewGuid().ToString("N");
var scanId = "scan-0004";
var mockService = new InMemoryVexGateQueryService();
mockService.AddScanResult(scanId, CreateTestGateResults(scanId, blockedCount: 5, warnCount: 10, passCount: 20));
using var factory = new ScannerApplicationFactory()
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<IVexGateQueryService>();
services.AddSingleton<IVexGateQueryService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/{scanId}/gate-blocked");
@@ -238,7 +247,7 @@ public sealed class VexGateEndpointsTests
Passed = passCount,
Warned = warnCount,
Blocked = blockedCount,
EvaluatedAt = DateTimeOffset.UtcNow,
EvaluatedAt = FixedNow,
},
GatedFindings = findings,
};
@@ -248,7 +257,7 @@ public sealed class VexGateEndpointsTests
{
return new GatedFindingDto
{
FindingId = $"finding-{Guid.NewGuid():N}",
FindingId = $"finding-{cve.ToLowerInvariant()}",
Cve = cve,
Purl = purl,
Decision = decision,
@@ -269,6 +278,8 @@ public sealed class VexGateEndpointsTests
},
};
}
private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
}
/// <summary>