201 lines
6.6 KiB
C#
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);
|
|
}
|
|
}
|