Files
git.stella-ops.org/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/CartographerWebhookClientTests.cs
2025-10-28 15:10:40 +02:00

141 lines
5.4 KiB
C#

using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.WebService.GraphJobs;
using StellaOps.Scheduler.WebService.Options;
namespace StellaOps.Scheduler.WebService.Tests;
public sealed class CartographerWebhookClientTests
{
[Fact]
public async Task NotifyAsync_PostsPayload_WhenEnabled()
{
var handler = new RecordingHandler();
var httpClient = new HttpClient(handler);
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerCartographerOptions
{
Webhook =
{
Enabled = true,
Endpoint = "https://cartographer.local/hooks/graph-completed",
ApiKeyHeader = "X-Api-Key",
ApiKey = "secret"
}
});
using var loggerFactory = LoggerFactory.Create(builder => builder.AddDebug());
var client = new CartographerWebhookClient(httpClient, new OptionsMonitorStub<SchedulerCartographerOptions>(options), loggerFactory.CreateLogger<CartographerWebhookClient>());
var job = new GraphBuildJob(
id: "gbj_test",
tenantId: "tenant-alpha",
sbomId: "sbom",
sbomVersionId: "sbom_v1",
sbomDigest: "sha256:" + new string('a', 64),
status: GraphJobStatus.Completed,
trigger: GraphBuildJobTrigger.Backfill,
createdAt: DateTimeOffset.UtcNow,
graphSnapshotId: "snap",
attempts: 1,
cartographerJobId: "carto-123",
correlationId: "corr-1",
startedAt: null,
completedAt: DateTimeOffset.UtcNow,
error: null,
metadata: Array.Empty<KeyValuePair<string, string>>());
var notification = new GraphJobCompletionNotification(
job.TenantId,
GraphJobQueryType.Build,
GraphJobStatus.Completed,
DateTimeOffset.UtcNow,
GraphJobResponse.From(job),
"oras://snap/result",
"corr-1",
null);
await client.NotifyAsync(notification, CancellationToken.None);
Assert.NotNull(handler.LastRequest);
Assert.Equal("https://cartographer.local/hooks/graph-completed", handler.LastRequest.RequestUri!.ToString());
Assert.True(handler.LastRequest.Headers.TryGetValues("X-Api-Key", out var values) && values!.Single() == "secret");
var json = JsonSerializer.Deserialize<JsonElement>(handler.LastPayload!);
Assert.Equal("gbj_test", json.GetProperty("jobId").GetString());
Assert.Equal("tenant-alpha", json.GetProperty("tenantId").GetString());
}
[Fact]
public async Task NotifyAsync_Skips_WhenDisabled()
{
var handler = new RecordingHandler();
var httpClient = new HttpClient(handler);
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerCartographerOptions());
using var loggerFactory = LoggerFactory.Create(builder => builder.AddDebug());
var client = new CartographerWebhookClient(httpClient, new OptionsMonitorStub<SchedulerCartographerOptions>(options), loggerFactory.CreateLogger<CartographerWebhookClient>());
var job = new GraphOverlayJob(
id: "goj-test",
tenantId: "tenant-alpha",
graphSnapshotId: "snap",
overlayKind: GraphOverlayKind.Policy,
overlayKey: "policy@1",
status: GraphJobStatus.Completed,
trigger: GraphOverlayJobTrigger.Manual,
createdAt: DateTimeOffset.UtcNow,
subjects: Array.Empty<string>(),
attempts: 1,
correlationId: null,
startedAt: null,
completedAt: DateTimeOffset.UtcNow,
error: null,
metadata: Array.Empty<KeyValuePair<string, string>>());
var notification = new GraphJobCompletionNotification(
job.TenantId,
GraphJobQueryType.Overlay,
GraphJobStatus.Completed,
DateTimeOffset.UtcNow,
GraphJobResponse.From(job),
null,
null,
null);
await client.NotifyAsync(notification, CancellationToken.None);
Assert.Null(handler.LastRequest);
}
private sealed class RecordingHandler : HttpMessageHandler
{
public HttpRequestMessage? LastRequest { get; private set; }
public string? LastPayload { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
LastRequest = request;
LastPayload = request.Content is null ? null : request.Content.ReadAsStringAsync(cancellationToken).Result;
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
}
}
private sealed class OptionsMonitorStub<T> : IOptionsMonitor<T> where T : class
{
private readonly IOptions<T> _options;
public OptionsMonitorStub(IOptions<T> options)
{
_options = options;
}
public T CurrentValue => _options.Value;
public T Get(string? name) => _options.Value;
public IDisposable? OnChange(Action<T, string?> listener) => null;
}
}