Files
git.stella-ops.org/src/Scheduler/__Tests/StellaOps.Scheduler.Persistence.Tests/SchedulerChainLinkingTests.cs
StellaOps Bot 37e11918e0 save progress
2026-01-06 09:42:20 +02:00

338 lines
12 KiB
C#

// <copyright file="SchedulerChainLinkingTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using StellaOps.HybridLogicalClock;
using Xunit;
namespace StellaOps.Scheduler.Persistence.Tests;
[Trait("Category", "Unit")]
public sealed class SchedulerChainLinkingTests
{
[Fact]
public void ComputeLink_WithNullPrevLink_UsesZeroLink()
{
// Arrange
var jobId = Guid.Parse("12345678-1234-1234-1234-123456789012");
var hlc = new HlcTimestamp { PhysicalTime = 1000000000000L, NodeId = "node1", LogicalCounter = 1 };
var payloadHash = new byte[32];
payloadHash[0] = 0xAB;
// Act
var link1 = SchedulerChainLinking.ComputeLink(null, jobId, hlc, payloadHash);
var link2 = SchedulerChainLinking.ComputeLink(SchedulerChainLinking.ZeroLink, jobId, hlc, payloadHash);
// Assert
link1.Should().HaveCount(32);
link1.Should().BeEquivalentTo(link2, "null prev_link should be treated as zero link");
}
[Fact]
public void ComputeLink_IsDeterministic_SameInputsSameOutput()
{
// Arrange
var prevLink = new byte[32];
prevLink[0] = 0x01;
var jobId = Guid.Parse("AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE");
var hlc = new HlcTimestamp { PhysicalTime = 1704067200000L, NodeId = "scheduler-1", LogicalCounter = 42 };
var payloadHash = new byte[32];
for (int i = 0; i < 32; i++) payloadHash[i] = (byte)i;
// Act
var link1 = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc, payloadHash);
var link2 = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc, payloadHash);
var link3 = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc, payloadHash);
// Assert
link1.Should().BeEquivalentTo(link2);
link2.Should().BeEquivalentTo(link3);
}
[Fact]
public void ComputeLink_DifferentJobIds_ProduceDifferentLinks()
{
// Arrange
var prevLink = new byte[32];
var hlc = new HlcTimestamp { PhysicalTime = 1704067200000L, NodeId = "node1", LogicalCounter = 1 };
var payloadHash = new byte[32];
var jobId1 = Guid.Parse("11111111-1111-1111-1111-111111111111");
var jobId2 = Guid.Parse("22222222-2222-2222-2222-222222222222");
// Act
var link1 = SchedulerChainLinking.ComputeLink(prevLink, jobId1, hlc, payloadHash);
var link2 = SchedulerChainLinking.ComputeLink(prevLink, jobId2, hlc, payloadHash);
// Assert
link1.Should().NotBeEquivalentTo(link2);
}
[Fact]
public void ComputeLink_DifferentHlcTimestamps_ProduceDifferentLinks()
{
// Arrange
var prevLink = new byte[32];
var jobId = Guid.NewGuid();
var payloadHash = new byte[32];
var hlc1 = new HlcTimestamp { PhysicalTime = 1704067200000L, NodeId = "node1", LogicalCounter = 1 };
var hlc2 = new HlcTimestamp { PhysicalTime = 1704067200000L, NodeId = "node1", LogicalCounter = 2 }; // Different counter
var hlc3 = new HlcTimestamp { PhysicalTime = 1704067200001L, NodeId = "node1", LogicalCounter = 1 }; // Different physical time
// Act
var link1 = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc1, payloadHash);
var link2 = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc2, payloadHash);
var link3 = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc3, payloadHash);
// Assert
link1.Should().NotBeEquivalentTo(link2);
link1.Should().NotBeEquivalentTo(link3);
link2.Should().NotBeEquivalentTo(link3);
}
[Fact]
public void ComputeLink_DifferentPrevLinks_ProduceDifferentLinks()
{
// Arrange
var jobId = Guid.NewGuid();
var hlc = new HlcTimestamp { PhysicalTime = 1704067200000L, NodeId = "node1", LogicalCounter = 1 };
var payloadHash = new byte[32];
var prevLink1 = new byte[32];
var prevLink2 = new byte[32];
prevLink2[0] = 0xFF;
// Act
var link1 = SchedulerChainLinking.ComputeLink(prevLink1, jobId, hlc, payloadHash);
var link2 = SchedulerChainLinking.ComputeLink(prevLink2, jobId, hlc, payloadHash);
// Assert
link1.Should().NotBeEquivalentTo(link2);
}
[Fact]
public void ComputeLink_DifferentPayloadHashes_ProduceDifferentLinks()
{
// Arrange
var prevLink = new byte[32];
var jobId = Guid.NewGuid();
var hlc = new HlcTimestamp { PhysicalTime = 1704067200000L, NodeId = "node1", LogicalCounter = 1 };
var payload1 = new byte[32];
var payload2 = new byte[32];
payload2[31] = 0x01;
// Act
var link1 = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc, payload1);
var link2 = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc, payload2);
// Assert
link1.Should().NotBeEquivalentTo(link2);
}
[Fact]
public void ComputeLink_WithStringHlc_ProducesSameResultAsParsedHlc()
{
// Arrange
var prevLink = new byte[32];
var jobId = Guid.NewGuid();
var hlc = new HlcTimestamp { PhysicalTime = 1704067200000L, NodeId = "node1", LogicalCounter = 42 };
var hlcString = hlc.ToSortableString();
var payloadHash = new byte[32];
// Act
var link1 = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc, payloadHash);
var link2 = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlcString, payloadHash);
// Assert
link1.Should().BeEquivalentTo(link2);
}
[Fact]
public void VerifyLink_ValidLink_ReturnsTrue()
{
// Arrange
var prevLink = new byte[32];
prevLink[0] = 0xDE;
var jobId = Guid.NewGuid();
var hlc = new HlcTimestamp { PhysicalTime = 1704067200000L, NodeId = "verifier", LogicalCounter = 100 };
var payloadHash = new byte[32];
payloadHash[15] = 0xAD;
var computedLink = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc, payloadHash);
// Act
var isValid = SchedulerChainLinking.VerifyLink(computedLink, prevLink, jobId, hlc, payloadHash);
// Assert
isValid.Should().BeTrue();
}
[Fact]
public void VerifyLink_TamperedLink_ReturnsFalse()
{
// Arrange
var prevLink = new byte[32];
var jobId = Guid.NewGuid();
var hlc = new HlcTimestamp { PhysicalTime = 1704067200000L, NodeId = "node1", LogicalCounter = 1 };
var payloadHash = new byte[32];
var computedLink = SchedulerChainLinking.ComputeLink(prevLink, jobId, hlc, payloadHash);
// Tamper with the link
var tamperedLink = (byte[])computedLink.Clone();
tamperedLink[0] ^= 0xFF;
// Act
var isValid = SchedulerChainLinking.VerifyLink(tamperedLink, prevLink, jobId, hlc, payloadHash);
// Assert
isValid.Should().BeFalse();
}
[Fact]
public void ComputePayloadHash_IsDeterministic()
{
// Arrange
var payload = new { Id = 123, Name = "Test", Values = new[] { 1, 2, 3 } };
// Act
var hash1 = SchedulerChainLinking.ComputePayloadHash(payload);
var hash2 = SchedulerChainLinking.ComputePayloadHash(payload);
// Assert
hash1.Should().HaveCount(32);
hash1.Should().BeEquivalentTo(hash2);
}
[Fact]
public void ComputePayloadHash_DifferentPayloads_ProduceDifferentHashes()
{
// Arrange
var payload1 = new { Id = 1, Name = "First" };
var payload2 = new { Id = 2, Name = "Second" };
// Act
var hash1 = SchedulerChainLinking.ComputePayloadHash(payload1);
var hash2 = SchedulerChainLinking.ComputePayloadHash(payload2);
// Assert
hash1.Should().NotBeEquivalentTo(hash2);
}
[Fact]
public void ComputePayloadHash_ByteArray_ProducesConsistentHash()
{
// Arrange
var bytes = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
// Act
var hash1 = SchedulerChainLinking.ComputePayloadHash(bytes);
var hash2 = SchedulerChainLinking.ComputePayloadHash(bytes);
// Assert
hash1.Should().HaveCount(32);
hash1.Should().BeEquivalentTo(hash2);
}
[Fact]
public void ToHex_NullLink_ReturnsNullString()
{
// Act
var result = SchedulerChainLinking.ToHex(null);
// Assert
result.Should().Be("(null)");
}
[Fact]
public void ToHex_EmptyLink_ReturnsNullString()
{
// Act
var result = SchedulerChainLinking.ToHex(Array.Empty<byte>());
// Assert
result.Should().Be("(null)");
}
[Fact]
public void ToHex_ValidLink_ReturnsLowercaseHex()
{
// Arrange
var link = new byte[] { 0xAB, 0xCD, 0xEF };
// Act
var result = SchedulerChainLinking.ToHex(link);
// Assert
result.Should().Be("abcdef");
}
[Fact]
public void ChainIntegrity_SequentialLinks_FormValidChain()
{
// Arrange - Simulate a chain of 5 entries
var jobIds = Enumerable.Range(1, 5).Select(i => Guid.NewGuid()).ToList();
var payloads = jobIds.Select(id => SchedulerChainLinking.ComputePayloadHash(new { JobId = id })).ToList();
var links = new List<byte[]>();
byte[]? prevLink = null;
long baseTime = 1704067200000L;
// Act - Build chain
for (int i = 0; i < 5; i++)
{
var hlc = new HlcTimestamp { PhysicalTime = baseTime + i, NodeId = "node1", LogicalCounter = i };
var link = SchedulerChainLinking.ComputeLink(prevLink, jobIds[i], hlc, payloads[i]);
links.Add(link);
prevLink = link;
}
// Assert - Verify chain integrity
byte[]? expectedPrev = null;
for (int i = 0; i < 5; i++)
{
var hlc = new HlcTimestamp { PhysicalTime = baseTime + i, NodeId = "node1", LogicalCounter = i };
var isValid = SchedulerChainLinking.VerifyLink(links[i], expectedPrev, jobIds[i], hlc, payloads[i]);
isValid.Should().BeTrue($"Link {i} should be valid");
expectedPrev = links[i];
}
}
[Fact]
public void ChainIntegrity_TamperedMiddleLink_BreaksChain()
{
// Arrange - Build a chain of 3 entries
var jobIds = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
var payloads = jobIds.Select(id => SchedulerChainLinking.ComputePayloadHash(new { JobId = id })).ToArray();
var hlcs = new[]
{
new HlcTimestamp { PhysicalTime = 1000L, NodeId = "node1", LogicalCounter = 0 },
new HlcTimestamp { PhysicalTime = 1001L, NodeId = "node1", LogicalCounter = 0 },
new HlcTimestamp { PhysicalTime = 1002L, NodeId = "node1", LogicalCounter = 0 }
};
var link0 = SchedulerChainLinking.ComputeLink(null, jobIds[0], hlcs[0], payloads[0]);
var link1 = SchedulerChainLinking.ComputeLink(link0, jobIds[1], hlcs[1], payloads[1]);
var link2 = SchedulerChainLinking.ComputeLink(link1, jobIds[2], hlcs[2], payloads[2]);
// Tamper with middle link
var tamperedLink1 = (byte[])link1.Clone();
tamperedLink1[0] ^= 0xFF;
// Act & Assert - First link is still valid
SchedulerChainLinking.VerifyLink(link0, null, jobIds[0], hlcs[0], payloads[0])
.Should().BeTrue("First link should be valid");
// Middle link verification fails
SchedulerChainLinking.VerifyLink(tamperedLink1, link0, jobIds[1], hlcs[1], payloads[1])
.Should().BeFalse("Tampered middle link should fail verification");
// Third link verification fails because prev_link is wrong
SchedulerChainLinking.VerifyLink(link2, tamperedLink1, jobIds[2], hlcs[2], payloads[2])
.Should().BeFalse("Third link should fail with tampered prev_link");
}
}