// ----------------------------------------------------------------------------- // EvidenceCompositionServiceTests.cs // Sprint: SPRINT_3800_0003_0001_evidence_api_endpoint // Description: Integration tests for Evidence API endpoints. // ----------------------------------------------------------------------------- using System.Net; using System.Net.Http.Json; using System.Text.Json; using StellaOps.Scanner.WebService.Contracts; using StellaOps.Scanner.WebService.Endpoints; using Xunit; using FluentAssertions; using StellaOps.TestKit; namespace StellaOps.Scanner.WebService.Tests; public sealed class EvidenceEndpointsTests { private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetEvidence_ReturnsBadRequest_WhenScanIdInvalid() { using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); // Empty scan ID - route doesn't match var response = await client.GetAsync("/api/v1/scans//evidence/CVE-2024-12345@pkg:npm/lodash@4.17.0"); response.StatusCode.Should().Be(HttpStatusCode.NotFound); // Route doesn't match } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetEvidence_ReturnsNotFound_WhenScanDoesNotExist() { using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); var response = await client.GetAsync( "/api/v1/scans/nonexistent-scan-id/evidence/CVE-2024-12345@pkg:npm/lodash@4.17.0"); response.StatusCode.Should().Be(HttpStatusCode.NotFound); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetEvidence_ReturnsListEndpoint_WhenFindingIdEmpty() { // When no finding ID is provided, the route matches the list endpoint using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); // Create a scan first var scanId = await CreateScanAsync(client); // Empty finding ID - route matches list endpoint var response = await client.GetAsync($"/api/v1/scans/{scanId}/evidence"); // Should return 200 OK with empty list (falls through to list endpoint) response.StatusCode.Should().Be(HttpStatusCode.OK); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ListEvidence_ReturnsEmptyList_WhenNoFindings() { using var secrets = new TestSurfaceSecretsScope(); using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); var scanId = await CreateScanAsync(client); var response = await client.GetAsync($"/api/v1/scans/{scanId}/evidence"); response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync(SerializerOptions); result.Should().NotBeNull(); result!.TotalCount.Should().Be(0); result.Items.Should().BeEmpty(); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ListEvidence_ReturnsEmptyList_WhenScanDoesNotExist() { // 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 => { configuration["scanner:authority:enabled"] = "false"; }); using var client = factory.CreateClient(); using StellaOps.TestKit; var response = await client.GetAsync("/api/v1/scans/nonexistent-scan/evidence"); // Current behavior: returns empty list (200 OK) for non-existent scans response.StatusCode.Should().Be(HttpStatusCode.OK); var result = await response.Content.ReadFromJsonAsync(SerializerOptions); result.Should().NotBeNull(); result!.TotalCount.Should().Be(0); } private static async Task CreateScanAsync(HttpClient client) { var createRequest = new ScanSubmitRequest { Image = new ScanImageDescriptor { Reference = "example.com/test:latest" } }; var createResponse = await client.PostAsJsonAsync("/api/v1/scans", createRequest); createResponse.EnsureSuccessStatusCode(); var createResult = await createResponse.Content.ReadFromJsonAsync(); return createResult.GetProperty("scanId").GetString()!; } } /// /// Tests for Evidence TTL and staleness handling (SPRINT_3800_0003_0002). /// public sealed class EvidenceTtlTests { [Trait("Category", TestCategories.Unit)] [Fact] public void DefaultEvidenceTtlDays_DefaultsToSevenDays() { // Verify the default configuration var options = new StellaOps.Scanner.WebService.Services.EvidenceCompositionOptions(); options.DefaultEvidenceTtlDays.Should().Be(7); } [Trait("Category", TestCategories.Unit)] [Fact] public void VexEvidenceTtlDays_DefaultsToThirtyDays() { var options = new StellaOps.Scanner.WebService.Services.EvidenceCompositionOptions(); options.VexEvidenceTtlDays.Should().Be(30); } [Trait("Category", TestCategories.Unit)] [Fact] public void StaleWarningThresholdDays_DefaultsToOne() { var options = new StellaOps.Scanner.WebService.Services.EvidenceCompositionOptions(); options.StaleWarningThresholdDays.Should().Be(1); } [Trait("Category", TestCategories.Unit)] [Fact] public void EvidenceCompositionOptions_CanBeConfigured() { var options = new StellaOps.Scanner.WebService.Services.EvidenceCompositionOptions { DefaultEvidenceTtlDays = 14, VexEvidenceTtlDays = 60, StaleWarningThresholdDays = 2 }; options.DefaultEvidenceTtlDays.Should().Be(14); options.VexEvidenceTtlDays.Should().Be(60); options.StaleWarningThresholdDays.Should().Be(2); } }