Add unit tests and implementations for MongoDB index models and OpenAPI metadata

- Implemented `MongoIndexModelTests` to verify index models for various stores.
- Created `OpenApiMetadataFactory` with methods to generate OpenAPI metadata.
- Added tests for `OpenApiMetadataFactory` to ensure expected defaults and URL overrides.
- Introduced `ObserverSurfaceSecrets` and `WebhookSurfaceSecrets` for managing secrets.
- Developed `RuntimeSurfaceFsClient` and `WebhookSurfaceFsClient` for manifest retrieval.
- Added dependency injection tests for `SurfaceEnvironmentRegistration` in both Observer and Webhook contexts.
- Implemented tests for secret resolution in `ObserverSurfaceSecretsTests` and `WebhookSurfaceSecretsTests`.
- Created `EnsureLinkNotMergeCollectionsMigrationTests` to validate MongoDB migration logic.
- Added project files for MongoDB tests and NuGet package mirroring.
This commit is contained in:
master
2025-11-17 21:21:56 +02:00
parent d3128aec24
commit 9075bad2d9
146 changed files with 152183 additions and 82 deletions

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Excititor.Core.Observations;
using Xunit;
namespace StellaOps.Excititor.Core.Tests.Observations;
public class TimelineEventTests
{
[Fact]
public void Normalizes_and_requires_fields()
{
var evt = new TimelineEvent(
eventId: " EVT-1 ",
tenant: "TenantA",
providerId: "prov",
streamId: "stream",
eventType: "ingest",
traceId: "trace-123",
justificationSummary: " summary ",
createdAt: DateTimeOffset.UnixEpoch,
evidenceHash: " evhash ",
payloadHash: " pwhash ",
attributes: ImmutableDictionary<string, string>.Empty.Add(" a ", " b " ));
evt.EventId.Should().Be("EVT-1");
evt.Tenant.Should().Be("tenanta");
evt.JustificationSummary.Should().Be("summary");
evt.EvidenceHash.Should().Be("evhash");
evt.PayloadHash.Should().Be("pwhash");
evt.Attributes.Should().ContainKey("a");
}
[Fact]
public void Throws_on_missing_required()
{
Action act = () => new TimelineEvent(" ", "t", "p", "s", "t", "trace", "", DateTimeOffset.UtcNow);
act.Should().Throw<ArgumentException>();
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Excititor.Core;
using Xunit;
namespace StellaOps.Excititor.Core.Tests;
public sealed class VexAttestationPayloadTests
{
[Fact]
public void Payload_NormalizesAndOrdersMetadata()
{
var metadata = ImmutableDictionary<string, string>.Empty
.Add(b,

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using EphemeralMongo;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Storage.Mongo;
using Xunit;
namespace StellaOps.Excititor.WebService.Tests;
public sealed class VexAttestationLinkEndpointTests : IDisposable
{
private readonly IMongoRunner _runner;
private readonly TestWebApplicationFactory _factory;
public VexAttestationLinkEndpointTests()
{
_runner = MongoRunner.Run(new MongoRunnerOptions { UseSingleNodeReplicaSet = true });
_factory = new TestWebApplicationFactory(
configureConfiguration: configuration =>
{
configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
[Excititor:Storage:Mongo:ConnectionString] = _runner.ConnectionString,
[Excititor:Storage:Mongo:DatabaseName] = vex_attestation_links,
[Excititor:Storage:Mongo:DefaultTenant] = tests,
});
},
configureServices: services =>
{
TestServiceOverrides.Apply(services);
services.AddTestAuthentication();
});
SeedLink();
}
[Fact]
public async Task GetAttestationLink_ReturnsPayload()
{
using var client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(Bearer, vex.read);
var response = await client.GetAsync(/v1/vex/attestations/att-123);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<VexAttestationPayload>();
Assert.NotNull(payload);
Assert.Equal(att-123, payload!.AttestationId);
Assert.Equal(supplier-a, payload.SupplierId);
Assert.Equal(CVE-2025-0001, payload.VulnerabilityId);
Assert.Equal(pkg:demo, payload.ProductKey);
}
private void SeedLink()
{
var client = new MongoDB.Driver.MongoClient(_runner.ConnectionString);
var database = client.GetDatabase(vex_attestation_links);
var collection = database.GetCollection<VexAttestationLinkRecord>(VexMongoCollectionNames.Attestations);
var record = new VexAttestationLinkRecord
{
AttestationId = att-123,
SupplierId = supplier-a,
ObservationId = obs-1,
LinksetId = link-1,
VulnerabilityId = CVE-2025-0001,
ProductKey = pkg:demo,
JustificationSummary = summary,
IssuedAt = DateTime.UtcNow,
Metadata = new Dictionary<string, string> { [policyRevisionId] = rev-1 },
};
collection.InsertOne(record);
}
public void Dispose()
{
_factory.Dispose();
_runner.Dispose();
}
}

View File

@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Services;
using Xunit;
namespace StellaOps.Excititor.WebService.Tests;
public sealed class VexEvidenceChunkServiceTests
{
[Fact]
public async Task QueryAsync_FiltersAndLimitsResults()
{
var now = new DateTimeOffset(2025, 11, 16, 12, 0, 0, TimeSpan.Zero);
var claims = new[]
{
CreateClaim("provider-a", VexClaimStatus.Affected, now.AddHours(-6), now.AddHours(-5), score: 0.9),
CreateClaim("provider-b", VexClaimStatus.NotAffected, now.AddHours(-4), now.AddHours(-3), score: 0.2)
};
var service = new VexEvidenceChunkService(new FakeClaimStore(claims), new FixedTimeProvider(now));
var request = new VexEvidenceChunkRequest(
Tenant: "tenant-a",
VulnerabilityId: "CVE-2025-0001",
ProductKey: "pkg:docker/demo",
ProviderIds: ImmutableHashSet.Create("provider-b"),
Statuses: ImmutableHashSet.Create(VexClaimStatus.NotAffected),
Since: null,
Limit: 1);
var result = await service.QueryAsync(request, CancellationToken.None);
result.Truncated.Should().BeTrue();
result.TotalCount.Should().Be(1);
result.GeneratedAtUtc.Should().Be(now);
var chunk = result.Chunks.Single();
chunk.ProviderId.Should().Be("provider-b");
chunk.Status.Should().Be(VexClaimStatus.NotAffected.ToString());
chunk.ScopeScore.Should().Be(0.2);
chunk.ObservationId.Should().Contain("provider-b");
chunk.Document.Digest.Should().NotBeNullOrWhiteSpace();
}
private static VexClaim CreateClaim(string providerId, VexClaimStatus status, DateTimeOffset firstSeen, DateTimeOffset lastSeen, double? score)
{
var product = new VexProduct("pkg:docker/demo", "demo", "1.0.0", "pkg:docker/demo:1.0.0", null, new[] { "component-a" });
var document = new VexClaimDocument(
VexDocumentFormat.SbomCycloneDx,
digest: Guid.NewGuid().ToString("N"),
sourceUri: new Uri("https://example.test/vex.json"),
revision: "r1",
signature: new VexSignatureMetadata("cosign", "demo", "issuer", keyId: "kid", verifiedAt: firstSeen, transparencyLogReference: null));
var signals = score.HasValue
? new VexSignalSnapshot(new VexSeveritySignal("cvss", score, "low", vector: null), Kev: null, Epss: null)
: null;
return new VexClaim(
"CVE-2025-0001",
providerId,
product,
status,
document,
firstSeen,
lastSeen,
justification: VexJustification.ComponentNotPresent,
detail: "demo detail",
confidence: null,
signals: signals,
additionalMetadata: ImmutableDictionary<string, string>.Empty);
}
private sealed class FakeClaimStore : IVexClaimStore
{
private readonly IReadOnlyCollection<VexClaim> _claims;
public FakeClaimStore(IReadOnlyCollection<VexClaim> claims)
{
_claims = claims;
}
public ValueTask AppendAsync(IEnumerable<VexClaim> claims, DateTimeOffset observedAt, CancellationToken cancellationToken, MongoDB.Driver.IClientSessionHandle? session = null)
=> throw new NotSupportedException();
public ValueTask<IReadOnlyCollection<VexClaim>> FindAsync(string vulnerabilityId, string productKey, DateTimeOffset? since, CancellationToken cancellationToken, MongoDB.Driver.IClientSessionHandle? session = null)
{
var query = _claims
.Where(claim => claim.VulnerabilityId == vulnerabilityId)
.Where(claim => claim.Product.Key == productKey);
if (since.HasValue)
{
query = query.Where(claim => claim.LastSeen >= since.Value);
}
return ValueTask.FromResult<IReadOnlyCollection<VexClaim>>(query.ToList());
}
}
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _timestamp;
public FixedTimeProvider(DateTimeOffset timestamp)
{
_timestamp = timestamp;
}
public override DateTimeOffset GetUtcNow() => _timestamp;
}
}

View File

@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using EphemeralMongo;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Contracts;
using Xunit;
namespace StellaOps.Excititor.WebService.Tests;
public sealed class VexEvidenceChunksEndpointTests : IDisposable
{
private readonly IMongoRunner _runner;
private readonly TestWebApplicationFactory _factory;
public VexEvidenceChunksEndpointTests()
{
_runner = MongoRunner.Run(new MongoRunnerOptions { UseSingleNodeReplicaSet = true });
_factory = new TestWebApplicationFactory(
configureConfiguration: configuration =>
{
configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["Excititor:Storage:Mongo:ConnectionString"] = _runner.ConnectionString,
["Excititor:Storage:Mongo:DatabaseName"] = "vex_chunks_tests",
["Excititor:Storage:Mongo:DefaultTenant"] = "tests",
});
},
configureServices: services =>
{
TestServiceOverrides.Apply(services);
services.AddTestAuthentication();
});
SeedStatements();
}
[Fact]
public async Task ChunksEndpoint_Filters_ByProvider_AndStreamsNdjson()
{
using var client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "vex.read");
client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tests");
var response = await client.GetAsync("/v1/vex/evidence/chunks?vulnerabilityId=CVE-2025-0001&productKey=pkg:docker/demo&providerId=provider-b&limit=1");
response.EnsureSuccessStatusCode();
Assert.True(response.Headers.TryGetValues("Excititor-Results-Truncated", out var truncatedValues));
Assert.Contains("true", truncatedValues, StringComparer.OrdinalIgnoreCase);
var body = await response.Content.ReadAsStringAsync();
var lines = body.Split(n, StringSplitOptions.RemoveEmptyEntries);
Assert.Single(lines);
var chunk = JsonSerializer.Deserialize<VexEvidenceChunkResponse>(lines[0], new JsonSerializerOptions(JsonSerializerDefaults.Web));
Assert.NotNull(chunk);
Assert.Equal("provider-b", chunk!.ProviderId);
Assert.Equal("NotAffected", chunk.Status);
Assert.Equal("pkg:docker/demo", chunk.Scope.Key);
Assert.Equal("CVE-2025-0001", chunk.VulnerabilityId);
}
private void SeedStatements()
{
var client = new MongoClient(_runner.ConnectionString);
var database = client.GetDatabase("vex_chunks_tests");
var collection = database.GetCollection<VexStatementRecord>(VexMongoCollectionNames.Statements);
var now = DateTimeOffset.UtcNow;
var claims = new[]
{
CreateClaim("provider-a", VexClaimStatus.Affected, now.AddHours(-6), now.AddHours(-5), 0.9),
CreateClaim("provider-b", VexClaimStatus.NotAffected, now.AddHours(-4), now.AddHours(-3), 0.2),
CreateClaim("provider-c", VexClaimStatus.Affected, now.AddHours(-2), now.AddHours(-1), 0.5)
};
var records = claims
.Select(claim => VexStatementRecord.FromDomain(claim, now))
.ToList();
collection.InsertMany(records);
}
private static VexClaim CreateClaim(string providerId, VexClaimStatus status, DateTimeOffset firstSeen, DateTimeOffset lastSeen, double? score)
{
var product = new VexProduct("pkg:docker/demo", "demo", "1.0.0", "pkg:docker/demo:1.0.0", null, new[] { "component-a" });
var document = new VexClaimDocument(
VexDocumentFormat.SbomCycloneDx,
digest: Guid.NewGuid().ToString("N"),
sourceUri: new Uri("https://example.test/vex.json"),
revision: "r1",
signature: new VexSignatureMetadata("cosign", "demo", "issuer", keyId: "kid", verifiedAt: firstSeen, transparencyLogReference: null));
var signals = score.HasValue
? new VexSignalSnapshot(new VexSeveritySignal("cvss", score, "low", vector: null), Kev: null, Epss: null)
: null;
return new VexClaim(
"CVE-2025-0001",
providerId,
product,
status,
document,
firstSeen,
lastSeen,
justification: VexJustification.ComponentNotPresent,
detail: "demo detail",
confidence: null,
signals: signals,
additionalMetadata: null);
}
public void Dispose()
{
_factory.Dispose();
_runner.Dispose();
}
}