338 lines
12 KiB
C#
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");
|
|
}
|
|
}
|