using System; using System.Collections.Immutable; using System.IO; using System.Text; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Vexer.Core; using StellaOps.Vexer.Export; using StellaOps.Vexer.Policy; using StellaOps.Vexer.Storage.Mongo; using Xunit; namespace StellaOps.Vexer.Export.Tests; public sealed class ExportEngineTests { [Fact] public async Task ExportAsync_GeneratesAndCachesManifest() { var store = new InMemoryExportStore(); var evaluator = new StaticPolicyEvaluator("baseline/v1"); var dataSource = new InMemoryExportDataSource(); var exporter = new DummyExporter(VexExportFormat.Json); var engine = new VexExportEngine(store, evaluator, dataSource, new[] { exporter }, NullLogger.Instance); var query = VexQuery.Create(new[] { new VexQueryFilter("vulnId", "CVE-2025-0001") }); var context = new VexExportRequestContext(query, VexExportFormat.Json, DateTimeOffset.UtcNow, ForceRefresh: false); var manifest = await engine.ExportAsync(context, CancellationToken.None); Assert.False(manifest.FromCache); Assert.Equal(VexExportFormat.Json, manifest.Format); Assert.Equal("baseline/v1", manifest.ConsensusRevision); Assert.Equal(1, manifest.ClaimCount); // second call hits cache var cached = await engine.ExportAsync(context, CancellationToken.None); Assert.True(cached.FromCache); Assert.Equal(manifest.ExportId, cached.ExportId); } private sealed class InMemoryExportStore : IVexExportStore { private readonly Dictionary _store = new(StringComparer.Ordinal); public ValueTask FindAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken) { var key = CreateKey(signature.Value, format); _store.TryGetValue(key, out var manifest); return ValueTask.FromResult(manifest); } public ValueTask SaveAsync(VexExportManifest manifest, CancellationToken cancellationToken) { var key = CreateKey(manifest.QuerySignature.Value, manifest.Format); _store[key] = manifest; return ValueTask.CompletedTask; } private static string CreateKey(string signature, VexExportFormat format) => FormattableString.Invariant($"{signature}|{format}"); } private sealed class StaticPolicyEvaluator : IVexPolicyEvaluator { public StaticPolicyEvaluator(string version) { Version = version; } public string Version { get; } public VexPolicySnapshot Snapshot => VexPolicySnapshot.Default; public double GetProviderWeight(VexProvider provider) => 1.0; public bool IsClaimEligible(VexClaim claim, VexProvider provider, out string? rejectionReason) { rejectionReason = null; return true; } } private sealed class InMemoryExportDataSource : IVexExportDataSource { public ValueTask FetchAsync(VexQuery query, CancellationToken cancellationToken) { var claim = new VexClaim( "CVE-2025-0001", "vendor", new VexProduct("pkg:demo/app", "Demo"), VexClaimStatus.Affected, new VexClaimDocument(VexDocumentFormat.Csaf, "sha256:demo", new Uri("https://example.org/demo")), DateTimeOffset.UtcNow, DateTimeOffset.UtcNow); var consensus = new VexConsensus( "CVE-2025-0001", claim.Product, VexConsensusStatus.Affected, DateTimeOffset.UtcNow, new[] { new VexConsensusSource("vendor", VexClaimStatus.Affected, "sha256:demo", 1.0) }, conflicts: null, policyVersion: "baseline/v1", summary: "affected"); return ValueTask.FromResult(new VexExportDataSet( ImmutableArray.Create(consensus), ImmutableArray.Create(claim), ImmutableArray.Create("vendor"))); } } private sealed class DummyExporter : IVexExporter { public DummyExporter(VexExportFormat format) { Format = format; } public VexExportFormat Format { get; } public VexContentAddress Digest(VexExportRequest request) => new("sha256", "deadbeef"); public ValueTask SerializeAsync(VexExportRequest request, Stream output, CancellationToken cancellationToken) { var bytes = System.Text.Encoding.UTF8.GetBytes("{}"); output.Write(bytes); return ValueTask.FromResult(new VexExportResult(new VexContentAddress("sha256", "deadbeef"), bytes.Length, ImmutableDictionary.Empty)); } } }