84 lines
3.3 KiB
C#
84 lines
3.3 KiB
C#
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Determinism;
|
|
using StellaOps.Signals.Models;
|
|
using StellaOps.Signals.Options;
|
|
using StellaOps.Signals.Services;
|
|
using Xunit;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Signals.Tests;
|
|
|
|
public class InMemoryEventsPublisherTests
|
|
{
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task PublishFactUpdatedAsync_EmitsStructuredEvent()
|
|
{
|
|
var logger = new TestLogger<InMemoryEventsPublisher>();
|
|
var options = new SignalsOptions();
|
|
options.Events.Driver = "inmemory";
|
|
options.Events.Stream = "signals.fact.updated.v1";
|
|
options.Events.DefaultTenant = "tenant-default";
|
|
|
|
var builder = new ReachabilityFactEventBuilder(options, TimeProvider.System, SystemGuidProvider.Instance);
|
|
var publisher = new InMemoryEventsPublisher(logger, builder);
|
|
|
|
var fact = new ReachabilityFactDocument
|
|
{
|
|
SubjectKey = "tenant:image@sha256:abc",
|
|
CallgraphId = "cg-123",
|
|
ComputedAt = System.DateTimeOffset.Parse("2025-11-18T12:00:00Z"),
|
|
States = new List<ReachabilityStateDocument>
|
|
{
|
|
new() { Target = "pkg:pypi/django", Reachable = true, Confidence = 0.9, Bucket = "runtime", Weight = 0.45 },
|
|
new() { Target = "pkg:pypi/requests", Reachable = false, Confidence = 0.2, Bucket = "runtime", Weight = 0.45 }
|
|
},
|
|
RuntimeFacts = new List<RuntimeFactDocument>
|
|
{
|
|
new() { SymbolId = "funcA", HitCount = 3 }
|
|
}
|
|
};
|
|
|
|
var envelope = builder.Build(fact);
|
|
await publisher.PublishFactUpdatedAsync(fact, CancellationToken.None);
|
|
|
|
Assert.Equal("signals.fact.updated.v1", envelope.Topic);
|
|
Assert.Equal("signals.fact.updated@v1", envelope.Version);
|
|
Assert.False(string.IsNullOrWhiteSpace(envelope.EventId));
|
|
Assert.Equal("tenant-default", envelope.Tenant);
|
|
Assert.Equal("tenant:image@sha256:abc", envelope.SubjectKey);
|
|
Assert.Equal("cg-123", envelope.CallgraphId);
|
|
Assert.Equal(1, envelope.Summary.ReachableCount);
|
|
Assert.Equal(1, envelope.Summary.UnreachableCount);
|
|
Assert.Equal(1, envelope.Summary.RuntimeFactsCount);
|
|
Assert.Equal("runtime", envelope.Summary.Bucket);
|
|
Assert.Equal(2, envelope.Summary.StateCount);
|
|
Assert.Contains("pkg:pypi/django", envelope.Summary.Targets);
|
|
Assert.Contains("pkg:pypi/requests", envelope.Summary.Targets);
|
|
Assert.Contains("signals.fact.updated.v1", logger.LastMessage);
|
|
}
|
|
|
|
private sealed class TestLogger<T> : ILogger<T>
|
|
{
|
|
public string LastMessage { get; private set; } = string.Empty;
|
|
|
|
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, System.Exception? exception, Func<TState, System.Exception?, string> formatter)
|
|
{
|
|
LastMessage = formatter(state, exception);
|
|
}
|
|
|
|
private sealed class NullScope : IDisposable
|
|
{
|
|
public static readonly NullScope Instance = new();
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
}
|