Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
cryptopro-linux-csp / build-and-test (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
154 lines
5.7 KiB
C#
154 lines
5.7 KiB
C#
using System.Collections.Generic;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Signals.Models;
|
|
using StellaOps.Signals.Options;
|
|
using StellaOps.Signals.Services;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Signals.Tests;
|
|
|
|
public class RouterEventsPublisherTests
|
|
{
|
|
[Fact]
|
|
public async Task PublishFactUpdatedAsync_SendsEnvelopeToRouter()
|
|
{
|
|
var options = CreateOptions();
|
|
var handler = new StubHandler(HttpStatusCode.Accepted);
|
|
using var httpClient = new HttpClient(handler) { BaseAddress = new Uri(options.Events.Router.BaseUrl) };
|
|
var logger = new ListLogger<RouterEventsPublisher>();
|
|
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System);
|
|
var publisher = new RouterEventsPublisher(builder, options, httpClient, logger);
|
|
|
|
await publisher.PublishFactUpdatedAsync(CreateFact(), CancellationToken.None);
|
|
|
|
var request = Assert.Single(handler.Requests);
|
|
Assert.Equal(options.Events.Router.Path, request.Uri!.PathAndQuery);
|
|
Assert.Equal("application/json", request.ContentType);
|
|
Assert.Contains(options.Events.Router.ApiKeyHeader, request.Headers.Keys);
|
|
|
|
using var doc = JsonDocument.Parse(request.Body ?? "{}");
|
|
Assert.Equal(options.Events.Stream, doc.RootElement.GetProperty("topic").GetString());
|
|
Assert.Equal("signals.fact.updated@v1", doc.RootElement.GetProperty("version").GetString());
|
|
Assert.Contains(logger.Messages, m => m.Contains("Router publish succeeded"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PublishFactUpdatedAsync_LogsFailure()
|
|
{
|
|
var options = CreateOptions();
|
|
var handler = new StubHandler(HttpStatusCode.InternalServerError, "boom");
|
|
using var httpClient = new HttpClient(handler) { BaseAddress = new Uri(options.Events.Router.BaseUrl) };
|
|
var logger = new ListLogger<RouterEventsPublisher>();
|
|
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System);
|
|
var publisher = new RouterEventsPublisher(builder, options, httpClient, logger);
|
|
|
|
await publisher.PublishFactUpdatedAsync(CreateFact(), CancellationToken.None);
|
|
|
|
Assert.Contains(logger.Messages, m => m.Contains("Router publish failed"));
|
|
}
|
|
|
|
private static SignalsOptions CreateOptions()
|
|
{
|
|
var options = new SignalsOptions();
|
|
options.Events.Driver = "router";
|
|
options.Events.Stream = "signals.fact.updated.v1";
|
|
options.Events.Router.BaseUrl = "https://router.test";
|
|
options.Events.Router.Path = "/router/events/signals.fact.updated";
|
|
options.Events.Router.ApiKeyHeader = "X-Test-Key";
|
|
options.Events.Router.ApiKey = "secret";
|
|
return options;
|
|
}
|
|
|
|
private static ReachabilityFactDocument CreateFact()
|
|
{
|
|
return new ReachabilityFactDocument
|
|
{
|
|
SubjectKey = "tenant:image@sha256:abc",
|
|
CallgraphId = "cg-123",
|
|
ComputedAt = DateTimeOffset.Parse("2025-12-10T00:00:00Z"),
|
|
States = new List<ReachabilityStateDocument>
|
|
{
|
|
new()
|
|
{
|
|
Target = "pkg:pypi/django",
|
|
Reachable = true,
|
|
Confidence = 0.9,
|
|
Bucket = "runtime",
|
|
Weight = 0.45
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private sealed class StubHandler : HttpMessageHandler
|
|
{
|
|
private readonly HttpStatusCode statusCode;
|
|
private readonly string? responseBody;
|
|
|
|
public List<CapturedRequest> Requests { get; } = new();
|
|
|
|
public StubHandler(HttpStatusCode statusCode, string? responseBody = null)
|
|
{
|
|
this.statusCode = statusCode;
|
|
this.responseBody = responseBody;
|
|
}
|
|
|
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
var body = request.Content is null
|
|
? null
|
|
: await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
Requests.Add(new CapturedRequest
|
|
{
|
|
Uri = request.RequestUri,
|
|
ContentType = request.Content?.Headers.ContentType?.MediaType,
|
|
Headers = request.Headers.ToDictionary(h => h.Key, h => h.Value.ToArray()),
|
|
Body = body
|
|
});
|
|
|
|
var response = new HttpResponseMessage(statusCode);
|
|
if (!string.IsNullOrEmpty(responseBody))
|
|
{
|
|
response.Content = new StringContent(responseBody);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
}
|
|
|
|
private sealed record CapturedRequest
|
|
{
|
|
public Uri? Uri { get; init; }
|
|
public string? ContentType { get; init; }
|
|
public Dictionary<string, string[]> Headers { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
|
public string? Body { get; init; }
|
|
}
|
|
|
|
private sealed class ListLogger<T> : ILogger<T>
|
|
{
|
|
public List<string> Messages { get; } = new();
|
|
|
|
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
|
|
|
public bool IsEnabled(LogLevel logLevel) => true;
|
|
|
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
|
{
|
|
Messages.Add(formatter(state, exception));
|
|
}
|
|
|
|
private sealed class NullScope : IDisposable
|
|
{
|
|
public static readonly NullScope Instance = new();
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
}
|