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:
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user