Files
git.stella-ops.org/src/__Libraries/__Tests/StellaOps.Eventing.Tests/TimelineEventEmitterTests.cs

201 lines
6.6 KiB
C#

// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using Moq;
using StellaOps.Eventing.Models;
using StellaOps.Eventing.Storage;
using StellaOps.HybridLogicalClock;
using Xunit;
namespace StellaOps.Eventing.Tests;
[Trait("Category", "Unit")]
public sealed class TimelineEventEmitterTests
{
private readonly FakeTimeProvider _timeProvider;
private readonly Mock<IHybridLogicalClock> _hlcMock;
private readonly InMemoryTimelineEventStore _eventStore;
private readonly IOptions<EventingOptions> _options;
private readonly TimelineEventEmitter _emitter;
public TimelineEventEmitterTests()
{
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero));
_hlcMock = new Mock<IHybridLogicalClock>();
_eventStore = new InMemoryTimelineEventStore();
_options = Options.Create(new EventingOptions
{
ServiceName = "TestService",
EngineVersion = new EngineVersionRef("TestEngine", "1.0.0", "sha256:test")
});
_emitter = new TimelineEventEmitter(
_hlcMock.Object,
_timeProvider,
_eventStore,
_options,
NullLogger<TimelineEventEmitter>.Instance);
}
private static HlcTimestamp CreateHlc(long physicalTime, int logicalCounter, string nodeId)
{
return new HlcTimestamp
{
PhysicalTime = physicalTime,
LogicalCounter = logicalCounter,
NodeId = nodeId
};
}
[Fact]
public async Task EmitAsync_StoresEventWithCorrectFields()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
var correlationId = "scan-abc123";
var kind = EventKinds.Enqueue;
var payload = new { JobId = "job-1", Status = "pending" };
var expectedHlc = CreateHlc(1704585600000, 0, "node1");
_hlcMock.Setup(h => h.Tick()).Returns(expectedHlc);
// Act
var result = await _emitter.EmitAsync(correlationId, kind, payload, ct);
// Assert
result.Should().NotBeNull();
result.CorrelationId.Should().Be(correlationId);
result.Kind.Should().Be(kind);
result.Service.Should().Be("TestService");
result.THlc.Should().Be(expectedHlc);
result.TsWall.Should().Be(_timeProvider.GetUtcNow());
result.SchemaVersion.Should().Be(1);
result.EngineVersion.EngineName.Should().Be("TestEngine");
result.EngineVersion.Version.Should().Be("1.0.0");
result.PayloadDigest.Should().NotBeEmpty();
result.EventId.Should().HaveLength(32);
}
[Fact]
public async Task EmitAsync_GeneratesDeterministicEventId()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
var correlationId = "scan-abc123";
var kind = EventKinds.Execute;
var payload = new { Step = 1 };
var hlc = CreateHlc(1704585600000, 0, "node1");
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
// Act
var result1 = await _emitter.EmitAsync(correlationId, kind, payload, ct);
// Create a second emitter with same config
var emitter2 = new TimelineEventEmitter(
_hlcMock.Object,
_timeProvider,
new InMemoryTimelineEventStore(),
_options,
NullLogger<TimelineEventEmitter>.Instance);
var result2 = await emitter2.EmitAsync(correlationId, kind, payload, ct);
// Assert - Same inputs should produce same EventId
result1.EventId.Should().Be(result2.EventId);
}
[Fact]
public async Task EmitAsync_StoresEventInStore()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
var correlationId = "scan-abc123";
var hlc = CreateHlc(1704585600000, 0, "node1");
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
// Act
var emitted = await _emitter.EmitAsync(correlationId, EventKinds.Enqueue, new { Test = true }, ct);
// Assert
var stored = await _eventStore.GetByIdAsync(emitted.EventId, ct);
stored.Should().NotBeNull();
stored!.EventId.Should().Be(emitted.EventId);
}
[Fact]
public async Task EmitBatchAsync_StoresAllEvents()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
var hlcCounter = 0;
_hlcMock.Setup(h => h.Tick())
.Returns(() => CreateHlc(1704585600000, hlcCounter++, "node1"));
var pendingEvents = new[]
{
new PendingEvent("scan-1", EventKinds.Enqueue, new { Step = 1 }),
new PendingEvent("scan-1", EventKinds.Execute, new { Step = 2 }),
new PendingEvent("scan-1", EventKinds.Complete, new { Step = 3 })
};
// Act
var results = await _emitter.EmitBatchAsync(pendingEvents, ct);
// Assert
results.Should().HaveCount(3);
results.Select(r => r.Kind).Should().BeEquivalentTo(
new[] { EventKinds.Enqueue, EventKinds.Execute, EventKinds.Complete });
var stored = _eventStore.GetAll();
stored.Should().HaveCount(3);
}
[Fact]
public async Task EmitBatchAsync_EmptyBatch_ReturnsEmptyList()
{
// Act
var ct = TestContext.Current.CancellationToken;
var results = await _emitter.EmitBatchAsync(Array.Empty<PendingEvent>(), ct);
// Assert
results.Should().BeEmpty();
}
[Fact]
public async Task EmitAsync_IncludesPayloadDigest()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
var hlc = CreateHlc(1704585600000, 0, "node1");
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
// Act
var result = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Data = "test" }, ct);
// Assert
result.PayloadDigest.Should().NotBeNull();
result.PayloadDigest.Should().HaveCount(32); // SHA-256
}
[Fact]
public async Task EmitAsync_DifferentPayloads_DifferentDigests()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
var hlcCounter = 0;
_hlcMock.Setup(h => h.Tick())
.Returns(() => CreateHlc(1704585600000, hlcCounter++, "node1"));
// Act
var result1 = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Value = 1 }, ct);
var result2 = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Value = 2 }, ct);
// Assert
result1.PayloadDigest.Should().NotBeEquivalentTo(result2.PayloadDigest);
}
}