notify doctors work, audit work, new product advisory sprints
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user