Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/EvidenceCompositionServiceTests.cs

189 lines
6.8 KiB
C#

// -----------------------------------------------------------------------------
// 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<EvidenceListResponse>(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<EvidenceListResponse>(SerializerOptions);
result.Should().NotBeNull();
result!.TotalCount.Should().Be(0);
}
private static async Task<string> 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<JsonElement>();
return createResult.GetProperty("scanId").GetString()!;
}
}
/// <summary>
/// Tests for Evidence TTL and staleness handling (SPRINT_3800_0003_0002).
/// </summary>
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);
}
}