Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ResolutionControllerIntegrationTests.cs
|
||||
// Sprint: SPRINT_1227_0001_0002_BE_resolution_api
|
||||
// Task: T9 — Integration tests for resolution API
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.BinaryIndex.Contracts.Resolution;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.WebService.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the Resolution API endpoints.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Category", "BinaryIndex")]
|
||||
public class ResolutionControllerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public ResolutionControllerIntegrationTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Add test-specific services if needed
|
||||
});
|
||||
});
|
||||
_client = _factory.CreateClient();
|
||||
}
|
||||
|
||||
#region Single Resolution Tests
|
||||
|
||||
[Fact(DisplayName = "POST /api/v1/resolve/vuln returns 200 for valid request")]
|
||||
public async Task ResolveVuln_ValidRequest_Returns200()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
BuildId = "abc123def456789",
|
||||
DistroRelease = "debian:bookworm"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Package.Should().Be("pkg:deb/debian/openssl@3.0.7");
|
||||
result.Status.Should().BeOneOf(ResolutionStatus.Fixed, ResolutionStatus.Vulnerable,
|
||||
ResolutionStatus.NotAffected, ResolutionStatus.Unknown);
|
||||
result.ResolvedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "POST /api/v1/resolve/vuln returns 400 for missing package")]
|
||||
public async Task ResolveVuln_MissingPackage_Returns400()
|
||||
{
|
||||
// Arrange
|
||||
var request = new { BuildId = "abc123" }; // Missing required Package field
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "POST /api/v1/resolve/vuln with CVE returns targeted resolution")]
|
||||
public async Task ResolveVuln_WithCveId_ReturnsTargetedResolution()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
CveId = "CVE-2024-0001",
|
||||
BuildId = "abc123def456789"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Resolution includes cache headers")]
|
||||
public async Task ResolveVuln_IncludesCacheHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Assert
|
||||
response.Headers.Should().ContainKey("X-RateLimit-Limit");
|
||||
response.Headers.Should().ContainKey("X-RateLimit-Remaining");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Resolution Tests
|
||||
|
||||
[Fact(DisplayName = "POST /api/v1/resolve/vuln/batch handles multiple items")]
|
||||
public async Task ResolveBatch_MultipleItems_ReturnsAllResults()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BatchVulnResolutionRequest
|
||||
{
|
||||
Items = new[]
|
||||
{
|
||||
new VulnResolutionRequest { Package = "pkg:deb/debian/openssl@3.0.7" },
|
||||
new VulnResolutionRequest { Package = "pkg:deb/debian/libcurl@7.88.1" },
|
||||
new VulnResolutionRequest { Package = "pkg:deb/debian/zlib@1.2.13" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln/batch", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<BatchVulnResolutionResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Results.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Batch resolution respects size limit")]
|
||||
public async Task ResolveBatch_ExceedsSizeLimit_Returns400()
|
||||
{
|
||||
// Arrange - Create 501 items (assuming 500 is the limit)
|
||||
var items = Enumerable.Range(0, 501)
|
||||
.Select(i => new VulnResolutionRequest { Package = $"pkg:npm/package{i}@1.0.0" })
|
||||
.ToArray();
|
||||
|
||||
var request = new BatchVulnResolutionRequest { Items = items };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln/batch", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Batch resolution performance under 500ms for 100 cached items")]
|
||||
public async Task ResolveBatch_CachedItems_PerformanceAcceptable()
|
||||
{
|
||||
// Arrange
|
||||
var items = Enumerable.Range(0, 100)
|
||||
.Select(i => new VulnResolutionRequest
|
||||
{
|
||||
Package = $"pkg:deb/debian/test-package{i}@1.0.0",
|
||||
BuildId = $"build-{i}"
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
var request = new BatchVulnResolutionRequest { Items = items };
|
||||
|
||||
// Warm up cache with first request
|
||||
await _client.PostAsJsonAsync("/api/v1/resolve/vuln/batch", request);
|
||||
|
||||
// Act
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln/batch", request);
|
||||
stopwatch.Stop();
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
stopwatch.ElapsedMilliseconds.Should().BeLessThan(500,
|
||||
"Cached batch resolution should complete in under 500ms");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cache Tests
|
||||
|
||||
[Fact(DisplayName = "Second request returns cached result")]
|
||||
public async Task ResolveVuln_SecondRequest_ReturnsCachedResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
BuildId = "cache-test-build-id"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response1 = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result1 = await response1.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
var response2 = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result2 = await response2.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
// Assert
|
||||
result1.Should().NotBeNull();
|
||||
result2.Should().NotBeNull();
|
||||
result2!.FromCache.Should().BeTrue();
|
||||
result1!.Status.Should().Be(result2.Status);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Bypass cache option works")]
|
||||
public async Task ResolveVuln_BypassCache_FreshResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
BuildId = "bypass-cache-test"
|
||||
};
|
||||
|
||||
// First request to populate cache
|
||||
await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Second request with bypass
|
||||
_client.DefaultRequestHeaders.Add("X-Bypass-Cache", "true");
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result = await response.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.FromCache.Should().BeFalse();
|
||||
|
||||
// Cleanup
|
||||
_client.DefaultRequestHeaders.Remove("X-Bypass-Cache");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DSSE Attestation Tests
|
||||
|
||||
[Fact(DisplayName = "Response includes DSSE attestation when requested")]
|
||||
public async Task ResolveVuln_WithDsseRequest_IncludesAttestation()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
BuildId = "dsse-test-build"
|
||||
};
|
||||
|
||||
_client.DefaultRequestHeaders.Add("X-Include-Attestation", "true");
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result = await response.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
// Note: Attestation may be null if signing is not configured
|
||||
|
||||
// Cleanup
|
||||
_client.DefaultRequestHeaders.Remove("X-Include-Attestation");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "DSSE attestation is valid base64")]
|
||||
public async Task ResolveVuln_DsseAttestation_IsValidBase64()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7",
|
||||
BuildId = "dsse-validation-test"
|
||||
};
|
||||
|
||||
_client.DefaultRequestHeaders.Add("X-Include-Attestation", "true");
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result = await response.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
// Assert
|
||||
if (!string.IsNullOrEmpty(result?.AttestationDsse))
|
||||
{
|
||||
// Should not throw
|
||||
var bytes = Convert.FromBase64String(result.AttestationDsse);
|
||||
bytes.Should().NotBeEmpty();
|
||||
|
||||
// Should be valid JSON
|
||||
var json = System.Text.Encoding.UTF8.GetString(bytes);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
doc.RootElement.TryGetProperty("payload", out _).Should().BeTrue();
|
||||
doc.RootElement.TryGetProperty("payloadType", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
_client.DefaultRequestHeaders.Remove("X-Include-Attestation");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rate Limiting Tests
|
||||
|
||||
[Fact(DisplayName = "Rate limiting returns 429 when exceeded")]
|
||||
public async Task ResolveVuln_RateLimitExceeded_Returns429()
|
||||
{
|
||||
// Arrange - This test depends on rate limit configuration
|
||||
// Create a client with test tenant that has low rate limit
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:npm/rate-limit-test@1.0.0"
|
||||
};
|
||||
|
||||
_client.DefaultRequestHeaders.Add("X-Tenant-Id", "rate-limit-test-tenant");
|
||||
|
||||
// Act - Send many requests quickly
|
||||
var tasks = Enumerable.Range(0, 150)
|
||||
.Select(_ => _client.PostAsJsonAsync("/api/v1/resolve/vuln", request));
|
||||
|
||||
var responses = await Task.WhenAll(tasks);
|
||||
|
||||
// Assert - At least some should be rate limited
|
||||
var rateLimited = responses.Where(r => r.StatusCode == HttpStatusCode.TooManyRequests);
|
||||
// Note: This may pass or fail depending on actual rate limit config
|
||||
|
||||
// Cleanup
|
||||
_client.DefaultRequestHeaders.Remove("X-Tenant-Id");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Rate limit headers are present")]
|
||||
public async Task ResolveVuln_RateLimitHeaders_Present()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:npm/headers-test@1.0.0"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
|
||||
// Assert
|
||||
response.Headers.Contains("X-RateLimit-Limit").Should().BeTrue();
|
||||
response.Headers.Contains("X-RateLimit-Remaining").Should().BeTrue();
|
||||
response.Headers.Contains("X-RateLimit-Reset").Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Evidence Tests
|
||||
|
||||
[Fact(DisplayName = "Fixed resolution includes evidence")]
|
||||
public async Task ResolveVuln_FixedStatus_IncludesEvidence()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VulnResolutionRequest
|
||||
{
|
||||
Package = "pkg:deb/debian/openssl@3.0.7-1+deb12u1",
|
||||
BuildId = "fixed-binary-build-id",
|
||||
DistroRelease = "debian:bookworm"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/resolve/vuln", request);
|
||||
var result = await response.Content.ReadFromJsonAsync<VulnResolutionResponse>();
|
||||
|
||||
// Assert
|
||||
if (result?.Status == ResolutionStatus.Fixed)
|
||||
{
|
||||
result.Evidence.Should().NotBeNull();
|
||||
result.Evidence!.MatchType.Should().NotBeNullOrEmpty();
|
||||
result.Evidence.Confidence.Should().BeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder for batch request if not in Contracts.
|
||||
/// </summary>
|
||||
public record BatchVulnResolutionRequest
|
||||
{
|
||||
public VulnResolutionRequest[] Items { get; init; } = Array.Empty<VulnResolutionRequest>();
|
||||
public ResolutionOptions? Options { get; init; }
|
||||
}
|
||||
|
||||
public record BatchVulnResolutionResponse
|
||||
{
|
||||
public VulnResolutionResponse[] Results { get; init; } = Array.Empty<VulnResolutionResponse>();
|
||||
public int TotalCount { get; init; }
|
||||
public int SuccessCount { get; init; }
|
||||
public int ErrorCount { get; init; }
|
||||
}
|
||||
|
||||
public record ResolutionOptions
|
||||
{
|
||||
public bool BypassCache { get; init; }
|
||||
public bool IncludeDsseAttestation { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user