REACH-013

This commit is contained in:
StellaOps Bot
2025-12-19 22:32:38 +02:00
parent 5b57b04484
commit edc91ea96f
5 changed files with 925 additions and 36 deletions

View File

@@ -0,0 +1,209 @@
using System.Collections.Immutable;
using System.Net;
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Observations;
using StellaOps.Excititor.Core.RiskFeed;
using StellaOps.Excititor.Core.Storage;
namespace StellaOps.Excititor.WebService.Tests;
/// <summary>
/// Tests for RiskFeedEndpoints (EXCITITOR-RISK-66-001).
/// </summary>
public sealed class RiskFeedEndpointsTests
{
private const string TestTenant = "test";
private const string TestAdvisoryKey = "CVE-2025-1234";
private const string TestArtifact = "pkg:maven/org.example/app@1.2.3";
[Fact]
public async Task GenerateFeed_ReturnsItems_ForValidRequest()
{
var linksets = CreateSampleLinksets();
var store = new StubLinksetStore(linksets);
var riskService = new RiskFeedService(store);
using var factory = new TestWebApplicationFactory(
configureServices: services =>
{
TestServiceOverrides.Apply(services);
services.RemoveAll<IRiskFeedService>();
services.AddSingleton<IRiskFeedService>(riskService);
services.AddTestAuthentication();
});
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "vex.read");
client.DefaultRequestHeaders.Add("X-Stella-Tenant", TestTenant);
var request = new { advisoryKeys = new[] { TestAdvisoryKey }, limit = 10 };
var response = await client.PostAsJsonAsync("/risk/v1/feed", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<RiskFeedResponseDto>();
Assert.NotNull(body);
Assert.NotEmpty(body!.Items);
Assert.Equal(TestAdvisoryKey, body.Items[0].AdvisoryKey);
}
[Fact]
public async Task GenerateFeed_ReturnsBadRequest_WhenNoBody()
{
using var factory = new TestWebApplicationFactory(
configureServices: services =>
{
TestServiceOverrides.Apply(services);
services.AddTestAuthentication();
});
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "vex.read");
client.DefaultRequestHeaders.Add("X-Stella-Tenant", TestTenant);
var response = await client.PostAsJsonAsync<object?>("/risk/v1/feed", null);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task GetItem_ReturnsItem_WhenFound()
{
var linksets = CreateSampleLinksets();
var store = new StubLinksetStore(linksets);
var riskService = new RiskFeedService(store);
using var factory = new TestWebApplicationFactory(
configureServices: services =>
{
TestServiceOverrides.Apply(services);
services.RemoveAll<IRiskFeedService>();
services.AddSingleton<IRiskFeedService>(riskService);
services.AddTestAuthentication();
});
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "vex.read");
client.DefaultRequestHeaders.Add("X-Stella-Tenant", TestTenant);
var response = await client.GetAsync($"/risk/v1/feed/item?advisoryKey={TestAdvisoryKey}&artifact={Uri.EscapeDataString(TestArtifact)}");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task GetItem_ReturnsNotFound_WhenMissing()
{
var riskService = new RiskFeedService(new StubLinksetStore(Array.Empty<VexLinkset>()));
using var factory = new TestWebApplicationFactory(
configureServices: services =>
{
TestServiceOverrides.Apply(services);
services.RemoveAll<IRiskFeedService>();
services.AddSingleton<IRiskFeedService>(riskService);
services.AddTestAuthentication();
});
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "vex.read");
client.DefaultRequestHeaders.Add("X-Stella-Tenant", TestTenant);
var response = await client.GetAsync("/risk/v1/feed/item?advisoryKey=CVE-9999-0000&artifact=pkg:fake/missing");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetFeedByAdvisory_ReturnsItems_ForValidAdvisory()
{
var linksets = CreateSampleLinksets();
var store = new StubLinksetStore(linksets);
var riskService = new RiskFeedService(store);
using var factory = new TestWebApplicationFactory(
configureServices: services =>
{
TestServiceOverrides.Apply(services);
services.RemoveAll<IRiskFeedService>();
services.AddSingleton<IRiskFeedService>(riskService);
services.AddTestAuthentication();
});
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "vex.read");
client.DefaultRequestHeaders.Add("X-Stella-Tenant", TestTenant);
var response = await client.GetAsync($"/risk/v1/feed/by-advisory/{TestAdvisoryKey}");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<RiskFeedResponseDto>();
Assert.NotNull(body);
}
private static IReadOnlyList<VexLinkset> CreateSampleLinksets()
{
var now = DateTimeOffset.Parse("2025-12-01T12:00:00Z");
var observation = new VexObservationRef(
observationId: "obs-001",
providerId: "ghsa",
status: "affected",
confidence: 0.9);
var linkset = new VexLinkset(
linksetId: VexLinkset.CreateLinksetId(TestTenant, TestAdvisoryKey, TestArtifact),
tenant: TestTenant,
vulnerabilityId: TestAdvisoryKey,
productKey: TestArtifact,
observations: ImmutableArray.Create(observation),
disagreements: ImmutableArray<VexDisagreement>.Empty,
confidence: 0.9,
hasConflicts: false,
createdAt: now.AddHours(-1),
updatedAt: now);
return new[] { linkset };
}
private sealed record RiskFeedResponseDto(
IReadOnlyList<RiskFeedItemDto> Items,
DateTimeOffset GeneratedAt);
private sealed record RiskFeedItemDto(
string AdvisoryKey,
string Artifact,
string Status);
private sealed class StubLinksetStore : IVexLinksetStore
{
private readonly IReadOnlyList<VexLinkset> _linksets;
public StubLinksetStore(IReadOnlyList<VexLinkset> linksets)
{
_linksets = linksets;
}
public ValueTask<VexLinkset?> GetByIdAsync(string tenant, string linksetId, CancellationToken cancellationToken)
=> ValueTask.FromResult(_linksets.FirstOrDefault(ls => ls.Tenant == tenant && ls.LinksetId == linksetId));
public ValueTask<IReadOnlyList<VexLinkset>> FindByVulnerabilityAsync(string tenant, string vulnerabilityId, int limit, CancellationToken cancellationToken)
=> ValueTask.FromResult<IReadOnlyList<VexLinkset>>(_linksets.Where(ls => ls.Tenant == tenant && ls.VulnerabilityId == vulnerabilityId).Take(limit).ToList());
public ValueTask<IReadOnlyList<VexLinkset>> FindByProductKeyAsync(string tenant, string productKey, int limit, CancellationToken cancellationToken)
=> ValueTask.FromResult<IReadOnlyList<VexLinkset>>(_linksets.Where(ls => ls.Tenant == tenant && ls.ProductKey == productKey).Take(limit).ToList());
public ValueTask<IReadOnlyList<VexLinkset>> FindWithConflictsAsync(string tenant, int limit, CancellationToken cancellationToken)
=> ValueTask.FromResult<IReadOnlyList<VexLinkset>>(_linksets.Where(ls => ls.Tenant == tenant && ls.HasConflicts).Take(limit).ToList());
public ValueTask UpsertAsync(VexLinkset linkset, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
public ValueTask<IReadOnlyList<VexLinkset>> FindAllAsync(string tenant, int limit, CancellationToken cancellationToken)
=> ValueTask.FromResult<IReadOnlyList<VexLinkset>>(_linksets.Where(ls => ls.Tenant == tenant).Take(limit).ToList());
}
}