stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
using FluentAssertions;
|
||||
using StellaOps.Eventing.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
public sealed partial class EventIdGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Generate_SameInputs_ProducesSameId()
|
||||
{
|
||||
var tHlc = CreateTimestamp();
|
||||
|
||||
var id1 = EventIdGenerator.Generate(DefaultCorrelationId, tHlc, DefaultService, DefaultKind);
|
||||
var id2 = EventIdGenerator.Generate(DefaultCorrelationId, tHlc, DefaultService, DefaultKind);
|
||||
|
||||
id1.Should().Be(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_DifferentCorrelationId_ProducesDifferentId()
|
||||
{
|
||||
var tHlc = CreateTimestamp();
|
||||
|
||||
var id1 = EventIdGenerator.Generate(DefaultCorrelationId, tHlc, DefaultService, DefaultKind);
|
||||
var id2 = EventIdGenerator.Generate("scan-xyz789", tHlc, DefaultService, DefaultKind);
|
||||
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_DifferentHlc_ProducesDifferentId()
|
||||
{
|
||||
var tHlc1 = CreateTimestamp();
|
||||
var tHlc2 = CreateTimestamp(1);
|
||||
|
||||
var id1 = EventIdGenerator.Generate(DefaultCorrelationId, tHlc1, DefaultService, DefaultKind);
|
||||
var id2 = EventIdGenerator.Generate(DefaultCorrelationId, tHlc2, DefaultService, DefaultKind);
|
||||
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_DifferentService_ProducesDifferentId()
|
||||
{
|
||||
var tHlc = CreateTimestamp();
|
||||
|
||||
var id1 = EventIdGenerator.Generate(DefaultCorrelationId, tHlc, DefaultService, DefaultKind);
|
||||
var id2 = EventIdGenerator.Generate(DefaultCorrelationId, tHlc, "AirGap", DefaultKind);
|
||||
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_DifferentKind_ProducesDifferentId()
|
||||
{
|
||||
var tHlc = CreateTimestamp();
|
||||
|
||||
var id1 = EventIdGenerator.Generate(DefaultCorrelationId, tHlc, DefaultService, DefaultKind);
|
||||
var id2 = EventIdGenerator.Generate(DefaultCorrelationId, tHlc, DefaultService, "EXECUTE");
|
||||
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_ReturnsLowercaseHex32Chars()
|
||||
{
|
||||
var tHlc = CreateTimestamp();
|
||||
|
||||
var id = EventIdGenerator.Generate(DefaultCorrelationId, tHlc, DefaultService, DefaultKind);
|
||||
|
||||
id.Should().HaveLength(32);
|
||||
id.Should().MatchRegex("^[a-f0-9]{32}$");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
using FluentAssertions;
|
||||
using StellaOps.Eventing.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
public sealed partial class EventIdGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputePayloadDigest_SamePayload_ProducesSameDigest()
|
||||
{
|
||||
var payload = """{"key":"value"}""";
|
||||
|
||||
var digest1 = EventIdGenerator.ComputePayloadDigest(payload);
|
||||
var digest2 = EventIdGenerator.ComputePayloadDigest(payload);
|
||||
|
||||
digest1.Should().BeEquivalentTo(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputePayloadDigest_DifferentPayload_ProducesDifferentDigest()
|
||||
{
|
||||
var payload1 = """{"key":"value1"}""";
|
||||
var payload2 = """{"key":"value2"}""";
|
||||
|
||||
var digest1 = EventIdGenerator.ComputePayloadDigest(payload1);
|
||||
var digest2 = EventIdGenerator.ComputePayloadDigest(payload2);
|
||||
|
||||
digest1.Should().NotBeEquivalentTo(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputePayloadDigest_Returns32Bytes()
|
||||
{
|
||||
var payload = """{"key":"value"}""";
|
||||
|
||||
var digest = EventIdGenerator.ComputePayloadDigest(payload);
|
||||
|
||||
digest.Should().HaveCount(32); // SHA-256 = 256 bits = 32 bytes
|
||||
}
|
||||
}
|
||||
@@ -1,189 +1,25 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Eventing.Internal;
|
||||
using StellaOps.HybridLogicalClock;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class EventIdGeneratorTests
|
||||
public sealed partial class EventIdGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Generate_SameInputs_ProducesSameId()
|
||||
private const string DefaultCorrelationId = "scan-abc123";
|
||||
private const string DefaultKind = "ENQUEUE";
|
||||
private const string DefaultNodeId = "node1";
|
||||
private const long DefaultPhysicalTime = 1704585600000;
|
||||
private const string DefaultService = "Scheduler";
|
||||
|
||||
private static HlcTimestamp CreateTimestamp(int logicalCounter = 0)
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = "scan-abc123";
|
||||
var tHlc = new HlcTimestamp
|
||||
return new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
PhysicalTime = DefaultPhysicalTime,
|
||||
LogicalCounter = logicalCounter,
|
||||
NodeId = DefaultNodeId
|
||||
};
|
||||
var service = "Scheduler";
|
||||
var kind = "ENQUEUE";
|
||||
|
||||
// Act
|
||||
var id1 = EventIdGenerator.Generate(correlationId, tHlc, service, kind);
|
||||
var id2 = EventIdGenerator.Generate(correlationId, tHlc, service, kind);
|
||||
|
||||
// Assert
|
||||
id1.Should().Be(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_DifferentCorrelationId_ProducesDifferentId()
|
||||
{
|
||||
// Arrange
|
||||
var tHlc = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var service = "Scheduler";
|
||||
var kind = "ENQUEUE";
|
||||
|
||||
// Act
|
||||
var id1 = EventIdGenerator.Generate("scan-abc123", tHlc, service, kind);
|
||||
var id2 = EventIdGenerator.Generate("scan-xyz789", tHlc, service, kind);
|
||||
|
||||
// Assert
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_DifferentHlc_ProducesDifferentId()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = "scan-abc123";
|
||||
var tHlc1 = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var tHlc2 = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 1,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var service = "Scheduler";
|
||||
var kind = "ENQUEUE";
|
||||
|
||||
// Act
|
||||
var id1 = EventIdGenerator.Generate(correlationId, tHlc1, service, kind);
|
||||
var id2 = EventIdGenerator.Generate(correlationId, tHlc2, service, kind);
|
||||
|
||||
// Assert
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_DifferentService_ProducesDifferentId()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = "scan-abc123";
|
||||
var tHlc = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var kind = "ENQUEUE";
|
||||
|
||||
// Act
|
||||
var id1 = EventIdGenerator.Generate(correlationId, tHlc, "Scheduler", kind);
|
||||
var id2 = EventIdGenerator.Generate(correlationId, tHlc, "AirGap", kind);
|
||||
|
||||
// Assert
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_DifferentKind_ProducesDifferentId()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = "scan-abc123";
|
||||
var tHlc = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var service = "Scheduler";
|
||||
|
||||
// Act
|
||||
var id1 = EventIdGenerator.Generate(correlationId, tHlc, service, "ENQUEUE");
|
||||
var id2 = EventIdGenerator.Generate(correlationId, tHlc, service, "EXECUTE");
|
||||
|
||||
// Assert
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_ReturnsLowercaseHex32Chars()
|
||||
{
|
||||
// Arrange
|
||||
var correlationId = "scan-abc123";
|
||||
var tHlc = new HlcTimestamp
|
||||
{
|
||||
PhysicalTime = 1704585600000,
|
||||
LogicalCounter = 0,
|
||||
NodeId = "node1"
|
||||
};
|
||||
var service = "Scheduler";
|
||||
var kind = "ENQUEUE";
|
||||
|
||||
// Act
|
||||
var id = EventIdGenerator.Generate(correlationId, tHlc, service, kind);
|
||||
|
||||
// Assert
|
||||
id.Should().HaveLength(32);
|
||||
id.Should().MatchRegex("^[a-f0-9]{32}$");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputePayloadDigest_SamePayload_ProducesSameDigest()
|
||||
{
|
||||
// Arrange
|
||||
var payload = """{"key":"value"}""";
|
||||
|
||||
// Act
|
||||
var digest1 = EventIdGenerator.ComputePayloadDigest(payload);
|
||||
var digest2 = EventIdGenerator.ComputePayloadDigest(payload);
|
||||
|
||||
// Assert
|
||||
digest1.Should().BeEquivalentTo(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputePayloadDigest_DifferentPayload_ProducesDifferentDigest()
|
||||
{
|
||||
// Arrange
|
||||
var payload1 = """{"key":"value1"}""";
|
||||
var payload2 = """{"key":"value2"}""";
|
||||
|
||||
// Act
|
||||
var digest1 = EventIdGenerator.ComputePayloadDigest(payload1);
|
||||
var digest2 = EventIdGenerator.ComputePayloadDigest(payload2);
|
||||
|
||||
// Assert
|
||||
digest1.Should().NotBeEquivalentTo(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputePayloadDigest_Returns32Bytes()
|
||||
{
|
||||
// Arrange
|
||||
var payload = """{"key":"value"}""";
|
||||
|
||||
// Act
|
||||
var digest = EventIdGenerator.ComputePayloadDigest(payload);
|
||||
|
||||
// Assert
|
||||
digest.Should().HaveCount(32); // SHA-256 = 256 bits = 32 bytes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
public sealed partial class InMemoryTimelineEventStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AppendAsync_StoresEventAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var e = CreateEvent("corr-1", "ENQUEUE", CreateHlc(1000, 0, "n1"));
|
||||
|
||||
await _store.AppendAsync(e, ct);
|
||||
|
||||
var retrieved = await _store.GetByIdAsync(e.EventId, ct);
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.EventId.Should().Be(e.EventId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppendAsync_Idempotent_DoesNotDuplicateAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var e = CreateEvent("corr-1", "ENQUEUE", CreateHlc(1000, 0, "n1"));
|
||||
|
||||
await _store.AppendAsync(e, ct);
|
||||
await _store.AppendAsync(e, ct);
|
||||
|
||||
var count = await _store.CountByCorrelationIdAsync("corr-1", ct);
|
||||
count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppendBatchAsync_StoresAllEventsAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var events = new[]
|
||||
{
|
||||
CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")),
|
||||
CreateEvent("corr-1", "B", CreateHlc(2000, 0, "n1")),
|
||||
CreateEvent("corr-1", "C", CreateHlc(3000, 0, "n1"))
|
||||
};
|
||||
|
||||
await _store.AppendBatchAsync(events, ct);
|
||||
|
||||
var count = await _store.CountByCorrelationIdAsync("corr-1", ct);
|
||||
count.Should().Be(3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
public sealed partial class InMemoryTimelineEventStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CountByCorrelationIdAsync_ReturnsCorrectCountAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "B", CreateHlc(2000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-2", "C", CreateHlc(3000, 0, "n1")), ct);
|
||||
|
||||
var count1 = await _store.CountByCorrelationIdAsync("corr-1", ct);
|
||||
var count2 = await _store.CountByCorrelationIdAsync("corr-2", ct);
|
||||
var count3 = await _store.CountByCorrelationIdAsync("corr-3", ct);
|
||||
|
||||
count1.Should().Be(2);
|
||||
count2.Should().Be(1);
|
||||
count3.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Clear_RemovesAllEventsAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")), ct);
|
||||
|
||||
_store.Clear();
|
||||
|
||||
_store.GetAll().Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
public sealed partial class InMemoryTimelineEventStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetByCorrelationIdAsync_ReturnsOrderedByHlcAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var hlc1 = CreateHlc(1000, 0, "n1");
|
||||
var hlc2 = CreateHlc(1000, 1, "n1");
|
||||
var hlc3 = CreateHlc(2000, 0, "n1");
|
||||
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "C", hlc3), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", hlc1), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "B", hlc2), ct);
|
||||
|
||||
var events = await _store.GetByCorrelationIdAsync("corr-1", cancellationToken: ct);
|
||||
|
||||
events.Should().HaveCount(3);
|
||||
events[0].Kind.Should().Be("A");
|
||||
events[1].Kind.Should().Be("B");
|
||||
events[2].Kind.Should().Be("C");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByCorrelationIdAsync_Pagination_WorksAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
await _store.AppendAsync(CreateEvent("corr-1", $"E{i}", CreateHlc(10000 + i, 0, "n1")), ct);
|
||||
}
|
||||
|
||||
var page1 = await _store.GetByCorrelationIdAsync("corr-1", limit: 3, offset: 0, cancellationToken: ct);
|
||||
var page2 = await _store.GetByCorrelationIdAsync("corr-1", limit: 3, offset: 3, cancellationToken: ct);
|
||||
|
||||
page1.Should().HaveCount(3);
|
||||
page2.Should().HaveCount(3);
|
||||
page1[0].Kind.Should().Be("E0");
|
||||
page2[0].Kind.Should().Be("E3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByHlcRangeAsync_FiltersCorrectlyAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "B", CreateHlc(2000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "C", CreateHlc(3000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "D", CreateHlc(4000, 0, "n1")), ct);
|
||||
|
||||
var events = await _store.GetByHlcRangeAsync(
|
||||
"corr-1",
|
||||
CreateHlc(2000, 0, "n1"),
|
||||
CreateHlc(3000, 0, "n1"),
|
||||
ct);
|
||||
|
||||
events.Should().HaveCount(2);
|
||||
events[0].Kind.Should().Be("B");
|
||||
events[1].Kind.Should().Be("C");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByServiceAsync_FiltersCorrectlyAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1"), "Scheduler"), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-2", "B", CreateHlc(2000, 0, "n1"), "AirGap"), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-3", "C", CreateHlc(3000, 0, "n1"), "Scheduler"), ct);
|
||||
|
||||
var events = await _store.GetByServiceAsync("Scheduler", cancellationToken: ct);
|
||||
|
||||
events.Should().HaveCount(2);
|
||||
events.All(e => e.Service == "Scheduler").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_NotFound_ReturnsNullAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _store.GetByIdAsync("nonexistent", ct);
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Eventing.Models;
|
||||
using StellaOps.Eventing.Storage;
|
||||
using StellaOps.HybridLogicalClock;
|
||||
@@ -9,8 +7,11 @@ using Xunit;
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class InMemoryTimelineEventStoreTests
|
||||
public sealed partial class InMemoryTimelineEventStoreTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedWallClock =
|
||||
new DateTimeOffset(2026, 1, 7, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private readonly InMemoryTimelineEventStore _store;
|
||||
|
||||
public InMemoryTimelineEventStoreTests()
|
||||
@@ -30,7 +31,7 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
CorrelationId = correlationId,
|
||||
Kind = kind,
|
||||
THlc = hlc,
|
||||
TsWall = DateTimeOffset.UtcNow,
|
||||
TsWall = FixedWallClock,
|
||||
Service = service,
|
||||
Payload = "{}",
|
||||
PayloadDigest = new byte[32],
|
||||
@@ -48,186 +49,4 @@ public sealed class InMemoryTimelineEventStoreTests
|
||||
NodeId = nodeId
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppendAsync_StoresEvent()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var e = CreateEvent("corr-1", "ENQUEUE", CreateHlc(1000, 0, "n1"));
|
||||
|
||||
// Act
|
||||
await _store.AppendAsync(e, ct);
|
||||
|
||||
// Assert
|
||||
var retrieved = await _store.GetByIdAsync(e.EventId, ct);
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.EventId.Should().Be(e.EventId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppendAsync_Idempotent_DoesNotDuplicate()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var e = CreateEvent("corr-1", "ENQUEUE", CreateHlc(1000, 0, "n1"));
|
||||
|
||||
// Act
|
||||
await _store.AppendAsync(e, ct);
|
||||
await _store.AppendAsync(e, ct); // Duplicate
|
||||
|
||||
// Assert
|
||||
var count = await _store.CountByCorrelationIdAsync("corr-1", ct);
|
||||
count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByCorrelationIdAsync_ReturnsOrderedByHlc()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var hlc1 = CreateHlc(1000, 0, "n1");
|
||||
var hlc2 = CreateHlc(1000, 1, "n1");
|
||||
var hlc3 = CreateHlc(2000, 0, "n1");
|
||||
|
||||
// Insert out of order
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "C", hlc3), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", hlc1), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "B", hlc2), ct);
|
||||
|
||||
// Act
|
||||
var events = await _store.GetByCorrelationIdAsync("corr-1", cancellationToken: ct);
|
||||
|
||||
// Assert
|
||||
events.Should().HaveCount(3);
|
||||
events[0].Kind.Should().Be("A");
|
||||
events[1].Kind.Should().Be("B");
|
||||
events[2].Kind.Should().Be("C");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByCorrelationIdAsync_Pagination_Works()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
await _store.AppendAsync(CreateEvent("corr-1", $"E{i}", CreateHlc(1000 + i, 0, "n1")), ct);
|
||||
}
|
||||
|
||||
// Act
|
||||
var page1 = await _store.GetByCorrelationIdAsync("corr-1", limit: 3, offset: 0, cancellationToken: ct);
|
||||
var page2 = await _store.GetByCorrelationIdAsync("corr-1", limit: 3, offset: 3, cancellationToken: ct);
|
||||
|
||||
// Assert
|
||||
page1.Should().HaveCount(3);
|
||||
page2.Should().HaveCount(3);
|
||||
page1[0].Kind.Should().Be("E0");
|
||||
page2[0].Kind.Should().Be("E3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByHlcRangeAsync_FiltersCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "B", CreateHlc(2000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "C", CreateHlc(3000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "D", CreateHlc(4000, 0, "n1")), ct);
|
||||
|
||||
// Act
|
||||
var events = await _store.GetByHlcRangeAsync(
|
||||
"corr-1",
|
||||
CreateHlc(2000, 0, "n1"),
|
||||
CreateHlc(3000, 0, "n1"),
|
||||
ct);
|
||||
|
||||
// Assert
|
||||
events.Should().HaveCount(2);
|
||||
events[0].Kind.Should().Be("B");
|
||||
events[1].Kind.Should().Be("C");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByServiceAsync_FiltersCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1"), "Scheduler"), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-2", "B", CreateHlc(2000, 0, "n1"), "AirGap"), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-3", "C", CreateHlc(3000, 0, "n1"), "Scheduler"), ct);
|
||||
|
||||
// Act
|
||||
var events = await _store.GetByServiceAsync("Scheduler", cancellationToken: ct);
|
||||
|
||||
// Assert
|
||||
events.Should().HaveCount(2);
|
||||
events.All(e => e.Service == "Scheduler").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_NotFound_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var result = await _store.GetByIdAsync("nonexistent", ct);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CountByCorrelationIdAsync_ReturnsCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "B", CreateHlc(2000, 0, "n1")), ct);
|
||||
await _store.AppendAsync(CreateEvent("corr-2", "C", CreateHlc(3000, 0, "n1")), ct);
|
||||
|
||||
// Act
|
||||
var count1 = await _store.CountByCorrelationIdAsync("corr-1", ct);
|
||||
var count2 = await _store.CountByCorrelationIdAsync("corr-2", ct);
|
||||
var count3 = await _store.CountByCorrelationIdAsync("corr-3", ct);
|
||||
|
||||
// Assert
|
||||
count1.Should().Be(2);
|
||||
count2.Should().Be(1);
|
||||
count3.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppendBatchAsync_StoresAllEvents()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var events = new[]
|
||||
{
|
||||
CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")),
|
||||
CreateEvent("corr-1", "B", CreateHlc(2000, 0, "n1")),
|
||||
CreateEvent("corr-1", "C", CreateHlc(3000, 0, "n1"))
|
||||
};
|
||||
|
||||
// Act
|
||||
await _store.AppendBatchAsync(events, ct);
|
||||
|
||||
// Assert
|
||||
var count = await _store.CountByCorrelationIdAsync("corr-1", ct);
|
||||
count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Clear_RemovesAllEvents()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
await _store.AppendAsync(CreateEvent("corr-1", "A", CreateHlc(1000, 0, "n1")), ct);
|
||||
|
||||
// Act
|
||||
_store.Clear();
|
||||
|
||||
// Assert
|
||||
_store.GetAll().Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Eventing.Models;
|
||||
using StellaOps.Eventing.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
public sealed partial class TimelineEventEmitterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EmitAsync_StoresEventWithCorrectFieldsAsync()
|
||||
{
|
||||
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);
|
||||
|
||||
var result = await _emitter.EmitAsync(correlationId, kind, payload, ct);
|
||||
|
||||
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_GeneratesDeterministicEventIdAsync()
|
||||
{
|
||||
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);
|
||||
|
||||
var result1 = await _emitter.EmitAsync(correlationId, kind, payload, ct);
|
||||
|
||||
var emitter2 = new TimelineEventEmitter(
|
||||
_hlcMock.Object,
|
||||
_timeProvider,
|
||||
new InMemoryTimelineEventStore(),
|
||||
_options,
|
||||
NullLogger<TimelineEventEmitter>.Instance);
|
||||
|
||||
var result2 = await emitter2.EmitAsync(correlationId, kind, payload, ct);
|
||||
|
||||
result1.EventId.Should().Be(result2.EventId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitAsync_StoresEventInStoreAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var correlationId = "scan-abc123";
|
||||
var hlc = CreateHlc(1704585600000, 0, "node1");
|
||||
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
|
||||
|
||||
var emitted = await _emitter.EmitAsync(correlationId, EventKinds.Enqueue, new { Test = true }, ct);
|
||||
|
||||
var stored = await _eventStore.GetByIdAsync(emitted.EventId, ct);
|
||||
stored.Should().NotBeNull();
|
||||
stored!.EventId.Should().Be(emitted.EventId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
using FluentAssertions;
|
||||
using StellaOps.Eventing.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
public sealed partial class TimelineEventEmitterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EmitBatchAsync_StoresAllEventsAsync()
|
||||
{
|
||||
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 })
|
||||
};
|
||||
|
||||
var results = await _emitter.EmitBatchAsync(pendingEvents, ct);
|
||||
|
||||
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_ReturnsEmptyListAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var results = await _emitter.EmitBatchAsync(Array.Empty<PendingEvent>(), ct);
|
||||
|
||||
results.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
using FluentAssertions;
|
||||
using StellaOps.Eventing.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
public sealed partial class TimelineEventEmitterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EmitAsync_IncludesPayloadDigestAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var hlc = CreateHlc(1704585600000, 0, "node1");
|
||||
_hlcMock.Setup(h => h.Tick()).Returns(hlc);
|
||||
|
||||
var result = await _emitter.EmitAsync("corr-1", EventKinds.Emit, new { Data = "test" }, ct);
|
||||
|
||||
result.PayloadDigest.Should().NotBeNull();
|
||||
result.PayloadDigest.Should().HaveCount(32);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitAsync_DifferentPayloads_DifferentDigestsAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var hlcCounter = 0;
|
||||
_hlcMock.Setup(h => h.Tick())
|
||||
.Returns(() => CreateHlc(1704585600000, hlcCounter++, "node1"));
|
||||
|
||||
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);
|
||||
|
||||
result1.PayloadDigest.Should().NotBeEquivalentTo(result2.PayloadDigest);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
// 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;
|
||||
@@ -13,7 +11,7 @@ using Xunit;
|
||||
namespace StellaOps.Eventing.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TimelineEventEmitterTests
|
||||
public sealed partial class TimelineEventEmitterTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly Mock<IHybridLogicalClock> _hlcMock;
|
||||
@@ -49,152 +47,4 @@ public sealed class TimelineEventEmitterTests
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user