213 lines
9.2 KiB
C#
213 lines
9.2 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Collections.Immutable;
|
|
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using EphemeralMongo;
|
|
using MongoRunner = EphemeralMongo.MongoRunner;
|
|
using MongoRunnerOptions = EphemeralMongo.MongoRunnerOptions;
|
|
using MongoDB.Driver;
|
|
using StellaOps.Excititor.Core;
|
|
using StellaOps.Excititor.Connectors.Abstractions;
|
|
using StellaOps.Excititor.Export;
|
|
using StellaOps.Excititor.Policy;
|
|
using StellaOps.Excititor.Storage.Mongo;
|
|
|
|
namespace StellaOps.Excititor.WebService.Tests;
|
|
|
|
public sealed class MirrorEndpointsTests : IDisposable
|
|
{
|
|
private readonly TestWebApplicationFactory _factory;
|
|
private readonly IMongoRunner _runner;
|
|
|
|
public MirrorEndpointsTests()
|
|
{
|
|
_runner = MongoRunner.Run(new MongoRunnerOptions { UseSingleNodeReplicaSet = true });
|
|
_factory = new TestWebApplicationFactory(
|
|
configureConfiguration: configuration =>
|
|
{
|
|
var data = new Dictionary<string, string?>
|
|
{
|
|
["Excititor:Storage:Mongo:ConnectionString"] = _runner.ConnectionString,
|
|
["Excititor:Storage:Mongo:DatabaseName"] = "mirror-tests",
|
|
[$"{MirrorDistributionOptions.SectionName}:Domains:0:Id"] = "primary",
|
|
[$"{MirrorDistributionOptions.SectionName}:Domains:0:DisplayName"] = "Primary Mirror",
|
|
[$"{MirrorDistributionOptions.SectionName}:Domains:0:MaxIndexRequestsPerHour"] = "1000",
|
|
[$"{MirrorDistributionOptions.SectionName}:Domains:0:MaxDownloadRequestsPerHour"] = "1000",
|
|
[$"{MirrorDistributionOptions.SectionName}:Domains:0:Exports:0:Key"] = "consensus",
|
|
[$"{MirrorDistributionOptions.SectionName}:Domains:0:Exports:0:Format"] = "json",
|
|
[$"{MirrorDistributionOptions.SectionName}:Domains:0:Exports:0:Filters:vulnId"] = "CVE-2025-0001",
|
|
[$"{MirrorDistributionOptions.SectionName}:Domains:0:Exports:0:Filters:productKey"] = "pkg:test/demo",
|
|
};
|
|
|
|
configuration.AddInMemoryCollection(data!);
|
|
},
|
|
configureServices: services =>
|
|
{
|
|
TestServiceOverrides.Apply(services);
|
|
services.RemoveAll<IVexExportStore>();
|
|
services.AddSingleton<IVexExportStore>(provider =>
|
|
{
|
|
var timeProvider = provider.GetRequiredService<TimeProvider>();
|
|
return new FakeExportStore(timeProvider);
|
|
});
|
|
services.RemoveAll<IVexArtifactStore>();
|
|
services.AddSingleton<IVexArtifactStore>(_ => new FakeArtifactStore());
|
|
services.AddSingleton(new VexConnectorDescriptor("excititor:redhat", VexProviderKind.Distro, "Red Hat CSAF"));
|
|
services.AddSingleton<StellaOps.Excititor.Attestation.Signing.IVexSigner, FakeSigner>();
|
|
services.AddSingleton<StellaOps.Excititor.Policy.IVexPolicyEvaluator, FakePolicyEvaluator>();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListDomains_ReturnsConfiguredDomain()
|
|
{
|
|
var client = _factory.CreateClient();
|
|
var response = await client.GetAsync("/excititor/mirror/domains");
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
|
var domains = document.RootElement.GetProperty("domains");
|
|
Assert.Equal(1, domains.GetArrayLength());
|
|
Assert.Equal("primary", domains[0].GetProperty("id").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DomainIndex_ReturnsManifestMetadata()
|
|
{
|
|
var client = _factory.CreateClient();
|
|
var response = await client.GetAsync("/excititor/mirror/domains/primary/index");
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
|
var exports = document.RootElement.GetProperty("exports");
|
|
Assert.Equal(1, exports.GetArrayLength());
|
|
var entry = exports[0];
|
|
Assert.Equal("consensus", entry.GetProperty("exportKey").GetString());
|
|
Assert.Equal("exports/20251019T000000000Z/abcdef", entry.GetProperty("exportId").GetString());
|
|
var artifact = entry.GetProperty("artifact");
|
|
Assert.Equal("sha256", artifact.GetProperty("algorithm").GetString());
|
|
Assert.Equal("deadbeef", artifact.GetProperty("digest").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Download_ReturnsArtifactContent()
|
|
{
|
|
var client = _factory.CreateClient();
|
|
var response = await client.GetAsync("/excititor/mirror/domains/primary/exports/consensus/download");
|
|
response.EnsureSuccessStatusCode();
|
|
Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType);
|
|
var payload = await response.Content.ReadAsStringAsync();
|
|
Assert.Equal("{\"status\":\"ok\"}", payload);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_factory.Dispose();
|
|
_runner.Dispose();
|
|
}
|
|
|
|
private sealed class FakeExportStore : IVexExportStore
|
|
{
|
|
private readonly ConcurrentDictionary<(string Signature, VexExportFormat Format), VexExportManifest> _manifests = new();
|
|
|
|
public FakeExportStore(TimeProvider timeProvider)
|
|
{
|
|
var filters = new[]
|
|
{
|
|
new VexQueryFilter("vulnId", "CVE-2025-0001"),
|
|
new VexQueryFilter("productKey", "pkg:test/demo"),
|
|
};
|
|
|
|
var query = VexQuery.Create(filters, Enumerable.Empty<VexQuerySort>());
|
|
var signature = VexQuerySignature.FromQuery(query);
|
|
var createdAt = new DateTimeOffset(2025, 10, 19, 0, 0, 0, TimeSpan.Zero);
|
|
|
|
var manifest = new VexExportManifest(
|
|
"exports/20251019T000000000Z/abcdef",
|
|
signature,
|
|
VexExportFormat.Json,
|
|
createdAt,
|
|
new VexContentAddress("sha256", "deadbeef"),
|
|
1,
|
|
new[] { "primary" },
|
|
fromCache: false,
|
|
consensusRevision: "rev-1",
|
|
attestation: new VexAttestationMetadata("https://stella-ops.org/attestations/vex-export"),
|
|
sizeBytes: 16);
|
|
|
|
_manifests.TryAdd((signature.Value, VexExportFormat.Json), manifest);
|
|
|
|
// Seed artifact content for download test.
|
|
FakeArtifactStore.Seed(manifest.Artifact, "{\"status\":\"ok\"}");
|
|
}
|
|
|
|
public ValueTask<VexExportManifest?> FindAsync(VexQuerySignature signature, VexExportFormat format, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
|
{
|
|
_manifests.TryGetValue((signature.Value, format), out var manifest);
|
|
return ValueTask.FromResult(manifest);
|
|
}
|
|
|
|
public ValueTask SaveAsync(VexExportManifest manifest, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
|
=> ValueTask.CompletedTask;
|
|
}
|
|
|
|
private sealed class FakeArtifactStore : IVexArtifactStore
|
|
{
|
|
private static readonly ConcurrentDictionary<VexContentAddress, byte[]> Content = new();
|
|
|
|
public static void Seed(VexContentAddress contentAddress, string payload)
|
|
{
|
|
var bytes = System.Text.Encoding.UTF8.GetBytes(payload);
|
|
Content[contentAddress] = bytes;
|
|
}
|
|
|
|
public ValueTask<VexStoredArtifact> SaveAsync(VexExportArtifact artifact, CancellationToken cancellationToken)
|
|
{
|
|
Content[artifact.ContentAddress] = artifact.Content.ToArray();
|
|
return ValueTask.FromResult(new VexStoredArtifact(artifact.ContentAddress, "memory://artifact", artifact.Content.Length, artifact.Metadata));
|
|
}
|
|
|
|
public ValueTask DeleteAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
|
|
{
|
|
Content.TryRemove(contentAddress, out _);
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public ValueTask<Stream?> OpenReadAsync(VexContentAddress contentAddress, CancellationToken cancellationToken)
|
|
{
|
|
if (!Content.TryGetValue(contentAddress, out var bytes))
|
|
{
|
|
return ValueTask.FromResult<Stream?>(null);
|
|
}
|
|
|
|
return ValueTask.FromResult<Stream?>(new MemoryStream(bytes, writable: false));
|
|
}
|
|
}
|
|
|
|
private sealed class FakeSigner : StellaOps.Excititor.Attestation.Signing.IVexSigner
|
|
{
|
|
public ValueTask<StellaOps.Excititor.Attestation.Signing.VexSignedPayload> SignAsync(ReadOnlyMemory<byte> payload, CancellationToken cancellationToken)
|
|
=> ValueTask.FromResult(new StellaOps.Excititor.Attestation.Signing.VexSignedPayload("signature", "key"));
|
|
}
|
|
|
|
private sealed class FakePolicyEvaluator : StellaOps.Excititor.Policy.IVexPolicyEvaluator
|
|
{
|
|
public string Version => "test";
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
}
|