// // Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later. // 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()); // 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[]? 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"); } }