using System.Collections.Immutable; using System.Net; using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using StellaOps.Scanner.SmartDiff.Detection; using StellaOps.Scanner.WebService.Services; using StellaOps.TestKit; namespace StellaOps.Scanner.WebService.Tests; [Trait("Category", TestCategories.Integration)] public sealed class SmartDiffEndpointsTests { private const string BasePath = "/api/v1/smart-diff"; private static readonly DateTimeOffset FixedNow = new(2026, 2, 12, 8, 0, 0, TimeSpan.Zero); [Fact] public async Task GetScanScopedVexCandidates_ReturnsCandidatesForResolvedTargetDigest() { var imageDigest = "sha256:target-123"; var candidate = CreateCandidate("candidate-1", imageDigest, requiresReview: true); var candidateStore = new InMemoryVexCandidateStore(); await candidateStore.StoreCandidatesAsync([candidate], TestContext.Current.CancellationToken); var metadataRepository = new StubScanMetadataRepository(); metadataRepository.Set("scan-1", new ScanMetadata(null, imageDigest, FixedNow)); await using var factory = CreateFactory(candidateStore, metadataRepository); await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/scan-1/vex-candidates", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); using var payload = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken), cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(imageDigest, payload.RootElement.GetProperty("imageDigest").GetString()); Assert.Equal(1, payload.RootElement.GetProperty("totalCandidates").GetInt32()); var first = payload.RootElement.GetProperty("candidates")[0]; Assert.Equal("candidate-1", first.GetProperty("candidateId").GetString()); Assert.Equal("CVE-2026-0001", first.GetProperty("vulnId").GetString()); Assert.Equal("vulnerable_code_not_in_execute_path", first.GetProperty("justification").GetString()); Assert.True(first.GetProperty("requiresReview").GetBoolean()); Assert.Equal("delta", first.GetProperty("evidenceLinks")[0].GetProperty("type").GetString()); } [Fact] public async Task ReviewScanScopedCandidate_UpdatesCandidateReviewState() { var imageDigest = "sha256:target-456"; var candidateStore = new InMemoryVexCandidateStore(); await candidateStore.StoreCandidatesAsync( [CreateCandidate("candidate-2", imageDigest, requiresReview: true)], TestContext.Current.CancellationToken); var metadataRepository = new StubScanMetadataRepository(); metadataRepository.Set("scan-2", new ScanMetadata(null, imageDigest, FixedNow)); await using var factory = CreateFactory(candidateStore, metadataRepository); await factory.InitializeAsync(); using var client = factory.CreateClient(); var reviewResponse = await client.PostAsJsonAsync( $"{BasePath}/scan-2/vex-candidates/review", new ScanReviewRequestDto("candidate-2", "accept", "verified"), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, reviewResponse.StatusCode); var candidateResponse = await client.GetAsync($"{BasePath}/candidates/candidate-2", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, candidateResponse.StatusCode); using var payload = await JsonDocument.ParseAsync(await candidateResponse.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken), cancellationToken: TestContext.Current.CancellationToken); var candidate = payload.RootElement.GetProperty("candidate"); Assert.False(candidate.GetProperty("requiresReview").GetBoolean()); } [Fact] public async Task GetScanSarif_EmbedsVexCandidateResults() { var imageDigest = "sha256:target-789"; var candidateStore = new InMemoryVexCandidateStore(); await candidateStore.StoreCandidatesAsync( [CreateCandidate("candidate-3", imageDigest, requiresReview: false)], TestContext.Current.CancellationToken); var metadataRepository = new StubScanMetadataRepository(); metadataRepository.Set("scan-3", new ScanMetadata("sha256:base-111", imageDigest, FixedNow)); await using var factory = CreateFactory(candidateStore, metadataRepository); await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/scans/scan-3/sarif", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); Assert.Contains("\"ruleId\":\"SDIFF003\"", body, StringComparison.Ordinal); Assert.Contains("CVE-2026-0001", body, StringComparison.Ordinal); Assert.Contains("pkg:npm/test-component@1.0.0", body, StringComparison.Ordinal); } private static ScannerApplicationFactory CreateFactory( IVexCandidateStore candidateStore, IScanMetadataRepository metadataRepository) { return ScannerApplicationFactory.CreateLightweight() .WithOverrides(configureServices: services => { services.RemoveAll(); services.RemoveAll(); services.RemoveAll(); services.AddSingleton(candidateStore); services.AddSingleton(); services.AddSingleton(metadataRepository); }); } private static VexCandidate CreateCandidate(string candidateId, string imageDigest, bool requiresReview) { return new VexCandidate( CandidateId: candidateId, FindingKey: new FindingKey("CVE-2026-0001", "pkg:npm/test-component@1.0.0"), SuggestedStatus: VexStatusType.NotAffected, Justification: VexJustification.VulnerableCodeNotInExecutePath, Rationale: "Reachability evidence shows vulnerable APIs are not reachable.", EvidenceLinks: ImmutableArray.Create(new EvidenceLink("delta", "oci://scanner/delta/scan-1", "sha256:evidence")), Confidence: 0.97, ImageDigest: imageDigest, GeneratedAt: FixedNow, ExpiresAt: FixedNow.AddDays(30), RequiresReview: requiresReview); } private sealed record ScanReviewRequestDto(string CandidateId, string Action, string? Comment); private sealed class StubScanMetadataRepository : IScanMetadataRepository { private readonly Dictionary _metadataByScanId = new(StringComparer.OrdinalIgnoreCase); public void Set(string scanId, ScanMetadata metadata) { _metadataByScanId[scanId] = metadata; } public Task GetScanMetadataAsync(string scanId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(_metadataByScanId.TryGetValue(scanId, out var metadata) ? metadata : null); } } private sealed class StubMaterialRiskChangeRepository : IMaterialRiskChangeRepository { public Task StoreChangeAsync(MaterialRiskChangeResult change, string scanId, CancellationToken ct = default) => Task.CompletedTask; public Task StoreChangesAsync(IReadOnlyList changes, string scanId, CancellationToken ct = default) => Task.CompletedTask; public Task> GetChangesForScanAsync(string scanId, CancellationToken ct = default) => Task.FromResult>(Array.Empty()); public Task> GetChangesForFindingAsync(FindingKey findingKey, int limit = 10, CancellationToken ct = default) => Task.FromResult>(Array.Empty()); public Task QueryChangesAsync(MaterialRiskChangeQuery query, CancellationToken ct = default) => Task.FromResult(new MaterialRiskChangeQueryResult(ImmutableArray.Empty, 0, query.Offset, query.Limit)); } }