feat: Initialize Zastava Webhook service with TLS and Authority authentication
- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint. - Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately. - Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly. - Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
This commit is contained in:
225
src/StellaOps.Excititor.WebService.Tests/MirrorEndpointsTests.cs
Normal file
225
src/StellaOps.Excititor.WebService.Tests/MirrorEndpointsTests.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
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;
|
||||
using StellaOps.Excititor.WebService.Options;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Tests;
|
||||
|
||||
public sealed class MirrorEndpointsTests : IClassFixture<WebApplicationFactory<Program>>, IDisposable
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly Mongo2Go.MongoDbRunner _runner;
|
||||
|
||||
public MirrorEndpointsTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_runner = Mongo2Go.MongoDbRunner.Start();
|
||||
_factory = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
var data = new Dictionary<string, string?>
|
||||
{
|
||||
[$"{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!);
|
||||
});
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveAll<IMongoClient>();
|
||||
services.AddSingleton<IMongoClient>(_ => new MongoClient(_runner.ConnectionString));
|
||||
services.RemoveAll<IMongoDatabase>();
|
||||
services.AddSingleton(provider => provider.GetRequiredService<IMongoClient>().GetDatabase("mirror-tests"));
|
||||
|
||||
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>();
|
||||
services.AddSingleton<IVexExportDataSource, FakeExportDataSource>();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
_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)
|
||||
{
|
||||
_manifests.TryGetValue((signature.Value, format), out var manifest);
|
||||
return ValueTask.FromResult(manifest);
|
||||
}
|
||||
|
||||
public ValueTask SaveAsync(VexExportManifest manifest, CancellationToken cancellationToken)
|
||||
=> 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;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeExportDataSource : IVexExportDataSource
|
||||
{
|
||||
public ValueTask<VexExportDataSet> FetchAsync(VexQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var dataset = new VexExportDataSet(ImmutableArray<VexConsensus>.Empty, ImmutableArray<VexClaim>.Empty, ImmutableArray<string>.Empty);
|
||||
return ValueTask.FromResult(dataset);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user