Add unit tests for RabbitMq and Udp transport servers and clients
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling.
- Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options.
- Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation.
- Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios.
- Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling.
- Included tests for UdpTransportOptions to verify default values and modification capabilities.
- Enhanced service registration tests for Udp transport services in the dependency injection container.
This commit is contained in:
master
2025-12-05 19:01:12 +02:00
parent 53508ceccb
commit cc69d332e3
245 changed files with 22440 additions and 27719 deletions

View File

@@ -69,8 +69,10 @@ public sealed record AuditEntry(
{
/// <summary>
/// Creates a new audit entry with computed hash.
/// Uses the platform's compliance-aware crypto abstraction.
/// </summary>
public static AuditEntry Create(
CanonicalJsonHasher hasher,
string tenantId,
AuditEventType eventType,
string resourceType,
@@ -89,12 +91,14 @@ public sealed record AuditEntry(
long sequenceNumber = 0,
string? metadata = null)
{
ArgumentNullException.ThrowIfNull(hasher);
var entryId = Guid.NewGuid();
var occurredAt = DateTimeOffset.UtcNow;
// Compute canonical hash from immutable content
// Use the same property names and fields as VerifyIntegrity to keep the hash stable.
var contentHash = CanonicalJsonHasher.ComputeCanonicalSha256(new
var contentHash = hasher.ComputeCanonicalHash(new
{
EntryId = entryId,
TenantId = tenantId,
@@ -135,10 +139,13 @@ public sealed record AuditEntry(
/// <summary>
/// Verifies the integrity of this entry's content hash.
/// Uses the platform's compliance-aware crypto abstraction.
/// </summary>
public bool VerifyIntegrity()
public bool VerifyIntegrity(CanonicalJsonHasher hasher)
{
var computed = CanonicalJsonHasher.ComputeCanonicalSha256(new
ArgumentNullException.ThrowIfNull(hasher);
var computed = hasher.ComputeCanonicalHash(new
{
EntryId,
TenantId,
@@ -169,12 +176,6 @@ public sealed record AuditEntry(
return string.Equals(PreviousEntryHash, previousEntry.ContentHash, StringComparison.OrdinalIgnoreCase);
}
private static string ComputeSha256(string content)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
var hash = System.Security.Cryptography.SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
/// <summary>

View File

@@ -1,7 +1,7 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core.Hashing;
namespace StellaOps.Orchestrator.Core.Domain.Events;
@@ -178,13 +178,18 @@ public sealed record EventEnvelope(
}
}
/// <summary>Computes a digest of the envelope for signing.</summary>
public string ComputeDigest()
/// <summary>
/// Computes a digest of the envelope for signing.
/// Uses the platform's compliance-aware crypto abstraction.
/// </summary>
public string ComputeDigest(ICryptoHash cryptoHash)
{
ArgumentNullException.ThrowIfNull(cryptoHash);
var canonicalJson = CanonicalJsonHasher.ToCanonicalJson(new { envelope = this });
var bytes = Encoding.UTF8.GetBytes(canonicalJson);
var hash = SHA256.HashData(bytes);
return $"sha256:{Convert.ToHexStringLower(hash)}";
var hash = cryptoHash.ComputePrefixedHashForPurpose(bytes, HashPurpose.Content);
return hash;
}
private static readonly JsonSerializerOptions JsonOptions = new()

View File

@@ -1,5 +1,5 @@
using System.Security.Cryptography;
using System.Text;
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core.Hashing;
namespace StellaOps.Orchestrator.Core.Domain;
@@ -44,8 +44,10 @@ public sealed record PackRunLog(
{
/// <summary>
/// Creates a new log entry.
/// Uses the platform's compliance-aware crypto abstraction.
/// </summary>
public static PackRunLog Create(
ICryptoHash cryptoHash,
Guid packRunId,
string tenantId,
long sequence,
@@ -55,7 +57,9 @@ public sealed record PackRunLog(
string? data = null,
DateTimeOffset? timestamp = null)
{
var (digest, sizeBytes) = ComputeDigest(message, data, tenantId, packRunId, sequence, level, source);
ArgumentNullException.ThrowIfNull(cryptoHash);
var (digest, sizeBytes) = ComputeDigest(cryptoHash, message, data, tenantId, packRunId, sequence, level, source);
return new PackRunLog(
LogId: Guid.NewGuid(),
@@ -75,32 +79,35 @@ public sealed record PackRunLog(
/// Creates an info-level stdout log entry.
/// </summary>
public static PackRunLog Stdout(
ICryptoHash cryptoHash,
Guid packRunId,
string tenantId,
long sequence,
string message,
DateTimeOffset? timestamp = null)
{
return Create(packRunId, tenantId, sequence, LogLevel.Info, "stdout", message, null, timestamp);
return Create(cryptoHash, packRunId, tenantId, sequence, LogLevel.Info, "stdout", message, null, timestamp);
}
/// <summary>
/// Creates a warn-level stderr log entry.
/// </summary>
public static PackRunLog Stderr(
ICryptoHash cryptoHash,
Guid packRunId,
string tenantId,
long sequence,
string message,
DateTimeOffset? timestamp = null)
{
return Create(packRunId, tenantId, sequence, LogLevel.Warn, "stderr", message, null, timestamp);
return Create(cryptoHash, packRunId, tenantId, sequence, LogLevel.Warn, "stderr", message, null, timestamp);
}
/// <summary>
/// Creates a system-level log entry (lifecycle events).
/// </summary>
public static PackRunLog System(
ICryptoHash cryptoHash,
Guid packRunId,
string tenantId,
long sequence,
@@ -109,10 +116,11 @@ public sealed record PackRunLog(
string? data = null,
DateTimeOffset? timestamp = null)
{
return Create(packRunId, tenantId, sequence, level, "system", message, data, timestamp);
return Create(cryptoHash, packRunId, tenantId, sequence, level, "system", message, data, timestamp);
}
private static (string Digest, long SizeBytes) ComputeDigest(
ICryptoHash cryptoHash,
string message,
string? data,
string tenantId,
@@ -134,9 +142,9 @@ public sealed record PackRunLog(
var canonicalJson = CanonicalJsonHasher.ToCanonicalJson(payload);
var bytes = Encoding.UTF8.GetBytes(canonicalJson);
var hash = SHA256.HashData(bytes);
var hash = cryptoHash.ComputeHashHexForPurpose(bytes, HashPurpose.Content);
return (Convert.ToHexString(hash).ToLowerInvariant(), bytes.LongLength);
return (hash, bytes.LongLength);
}
}

View File

@@ -18,15 +18,17 @@ public sealed record ReplayInputsLock(
public static ReplayInputsLock Create(
ReplayManifest manifest,
CanonicalJsonHasher hasher,
string? notes = null,
DateTimeOffset? createdAt = null,
string schemaVersion = DefaultSchemaVersion)
{
ArgumentNullException.ThrowIfNull(manifest);
ArgumentNullException.ThrowIfNull(hasher);
return new ReplayInputsLock(
SchemaVersion: schemaVersion,
ManifestHash: manifest.ComputeHash(),
ManifestHash: manifest.ComputeHash(hasher),
CreatedAt: createdAt ?? DateTimeOffset.UtcNow,
Inputs: manifest.Inputs,
Notes: string.IsNullOrWhiteSpace(notes) ? null : notes);
@@ -34,6 +36,11 @@ public sealed record ReplayInputsLock(
/// <summary>
/// Canonical hash of the lock content.
/// Uses the platform's compliance-aware crypto abstraction.
/// </summary>
public string ComputeHash() => CanonicalJsonHasher.ComputeCanonicalSha256(this);
public string ComputeHash(CanonicalJsonHasher hasher)
{
ArgumentNullException.ThrowIfNull(hasher);
return hasher.ComputeCanonicalHash(this);
}
}

View File

@@ -41,9 +41,14 @@ public sealed record ReplayManifest(
}
/// <summary>
/// Deterministic SHA-256 over canonical JSON representation of the manifest.
/// Deterministic hash over canonical JSON representation of the manifest.
/// Uses the platform's compliance-aware crypto abstraction.
/// </summary>
public string ComputeHash() => CanonicalJsonHasher.ComputeCanonicalSha256(this);
public string ComputeHash(CanonicalJsonHasher hasher)
{
ArgumentNullException.ThrowIfNull(hasher);
return hasher.ComputeCanonicalHash(this);
}
}
public sealed record ReplayInputs(

View File

@@ -1,17 +1,20 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using StellaOps.Cryptography;
namespace StellaOps.Orchestrator.Core.Hashing;
/// <summary>
/// Produces deterministic, canonical JSON and hashes for orchestrator payloads (events, audit, manifests).
/// Keys are sorted lexicographically; arrays preserve order; nulls are retained; timestamps remain ISO 8601 with offsets.
/// Uses compliance-profile-aware hashing via <see cref="ICryptoHash"/>.
/// </summary>
public static class CanonicalJsonHasher
public sealed class CanonicalJsonHasher
{
private readonly ICryptoHash _cryptoHash;
private static readonly JsonSerializerOptions SerializerOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
@@ -20,6 +23,15 @@ public static class CanonicalJsonHasher
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
/// <summary>
/// Creates a new CanonicalJsonHasher with the specified crypto hash service.
/// </summary>
/// <param name="cryptoHash">Crypto hash service for compliance-aware hashing.</param>
public CanonicalJsonHasher(ICryptoHash cryptoHash)
{
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
}
/// <summary>
/// Serialize the value to canonical JSON (sorted object keys, stable formatting).
/// </summary>
@@ -32,14 +44,14 @@ public static class CanonicalJsonHasher
}
/// <summary>
/// Compute SHA-256 over canonical JSON (lowercase hex).
/// Compute hash over canonical JSON using the active compliance profile (lowercase hex).
/// Uses <see cref="HashPurpose.Content"/> for content hashing.
/// </summary>
public static string ComputeCanonicalSha256<T>(T value)
public string ComputeCanonicalHash<T>(T value)
{
var canonicalJson = ToCanonicalJson(value);
var bytes = Encoding.UTF8.GetBytes(canonicalJson);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
return _cryptoHash.ComputeHashHexForPurpose(bytes, HashPurpose.Content);
}
private static JsonNode OrderNode(JsonNode node)

View File

@@ -2,11 +2,21 @@ using StellaOps.Orchestrator.Core.Domain.Events;
namespace StellaOps.Orchestrator.Core.Hashing;
public static class EventEnvelopeHasher
/// <summary>
/// Computes compliance-aware hashes for event envelopes using the platform's crypto abstraction.
/// </summary>
public sealed class EventEnvelopeHasher
{
public static string Compute(EventEnvelope envelope)
private readonly CanonicalJsonHasher _hasher;
public EventEnvelopeHasher(CanonicalJsonHasher hasher)
{
_hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
}
public string Compute(EventEnvelope envelope)
{
ArgumentNullException.ThrowIfNull(envelope);
return CanonicalJsonHasher.ComputeCanonicalSha256(envelope);
return _hasher.ComputeCanonicalHash(envelope);
}
}

View File

@@ -17,4 +17,8 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>

View File

@@ -2,6 +2,7 @@ using System.Text;
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Orchestrator.Core.Domain;
using StellaOps.Orchestrator.Core.Hashing;
using StellaOps.Orchestrator.Infrastructure.Repositories;
namespace StellaOps.Orchestrator.Infrastructure.Postgres;
@@ -61,13 +62,16 @@ public sealed class PostgresAuditRepository : IAuditRepository
""";
private readonly OrchestratorDataSource _dataSource;
private readonly CanonicalJsonHasher _hasher;
private readonly ILogger<PostgresAuditRepository> _logger;
public PostgresAuditRepository(
OrchestratorDataSource dataSource,
CanonicalJsonHasher hasher,
ILogger<PostgresAuditRepository> logger)
{
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
_hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -115,6 +119,7 @@ public sealed class PostgresAuditRepository : IAuditRepository
// Create the entry
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: tenantId,
eventType: eventType,
resourceType: resourceType,

View File

@@ -1,4 +1,6 @@
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core.Domain;
using StellaOps.Orchestrator.Core.Hashing;
namespace StellaOps.Orchestrator.Tests.AuditLedger;
@@ -7,6 +9,13 @@ namespace StellaOps.Orchestrator.Tests.AuditLedger;
/// </summary>
public sealed class AuditEntryTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
private readonly CanonicalJsonHasher _hasher;
public AuditEntryTests()
{
_hasher = new CanonicalJsonHasher(_cryptoHash);
}
[Fact]
public void Create_WithValidParameters_SetsAllProperties()
{
@@ -16,6 +25,7 @@ public sealed class AuditEntryTests
// Act
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: tenantId,
eventType: AuditEventType.JobCreated,
resourceType: "job",
@@ -62,6 +72,7 @@ public sealed class AuditEntryTests
{
// Arrange & Act
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.RunCreated,
resourceType: "run",
@@ -82,6 +93,7 @@ public sealed class AuditEntryTests
{
// Arrange
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.SourceCreated,
resourceType: "source",
@@ -92,7 +104,7 @@ public sealed class AuditEntryTests
sequenceNumber: 5);
// Act
var isValid = entry.VerifyIntegrity();
var isValid = entry.VerifyIntegrity(_hasher);
// Assert
Assert.True(isValid);
@@ -103,6 +115,7 @@ public sealed class AuditEntryTests
{
// Arrange
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.QuotaCreated,
resourceType: "quota",
@@ -116,7 +129,7 @@ public sealed class AuditEntryTests
var tamperedEntry = entry with { Description = "Tampered description" };
// Act
var isValid = tamperedEntry.VerifyIntegrity();
var isValid = tamperedEntry.VerifyIntegrity(_hasher);
// Assert
Assert.False(isValid);
@@ -127,6 +140,7 @@ public sealed class AuditEntryTests
{
// Arrange
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.JobScheduled,
resourceType: "job",
@@ -149,6 +163,7 @@ public sealed class AuditEntryTests
{
// Arrange
var first = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.JobCreated,
resourceType: "job",
@@ -160,6 +175,7 @@ public sealed class AuditEntryTests
sequenceNumber: 1);
var second = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.JobLeased,
resourceType: "job",
@@ -182,6 +198,7 @@ public sealed class AuditEntryTests
{
// Arrange
var first = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.JobCreated,
resourceType: "job",
@@ -193,6 +210,7 @@ public sealed class AuditEntryTests
sequenceNumber: 1);
var second = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.JobCompleted,
resourceType: "job",
@@ -225,6 +243,7 @@ public sealed class AuditEntryTests
{
// Act
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: eventType,
resourceType: resourceType,
@@ -237,7 +256,7 @@ public sealed class AuditEntryTests
// Assert
Assert.Equal(eventType, entry.EventType);
Assert.Equal(resourceType, entry.ResourceType);
Assert.True(entry.VerifyIntegrity());
Assert.True(entry.VerifyIntegrity(_hasher));
}
[Theory]
@@ -251,6 +270,7 @@ public sealed class AuditEntryTests
{
// Act
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.JobCreated,
resourceType: "job",
@@ -262,7 +282,7 @@ public sealed class AuditEntryTests
// Assert
Assert.Equal(actorType, entry.ActorType);
Assert.True(entry.VerifyIntegrity());
Assert.True(entry.VerifyIntegrity(_hasher));
}
[Fact]
@@ -274,6 +294,7 @@ public sealed class AuditEntryTests
// Act
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.JobLeased,
resourceType: "job",
@@ -295,6 +316,7 @@ public sealed class AuditEntryTests
{
// Act
var entry1 = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.JobCreated,
resourceType: "job",
@@ -305,6 +327,7 @@ public sealed class AuditEntryTests
sequenceNumber: 1);
var entry2 = AuditEntry.Create(
hasher: _hasher,
tenantId: "test-tenant",
eventType: AuditEventType.JobCreated,
resourceType: "job",

View File

@@ -1,4 +1,5 @@
using System.Text.Json;
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core.Domain;
using StellaOps.Orchestrator.Core.Hashing;
@@ -6,14 +7,22 @@ namespace StellaOps.Orchestrator.Tests;
public class CanonicalJsonHasherTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
private readonly CanonicalJsonHasher _hasher;
public CanonicalJsonHasherTests()
{
_hasher = new CanonicalJsonHasher(_cryptoHash);
}
[Fact]
public void ProducesStableHash_WhenObjectPropertyOrderDiffers()
{
var first = new { b = 1, a = 2 };
var second = new { a = 2, b = 1 };
var firstHash = CanonicalJsonHasher.ComputeCanonicalSha256(first);
var secondHash = CanonicalJsonHasher.ComputeCanonicalSha256(second);
var firstHash = _hasher.ComputeCanonicalHash(first);
var secondHash = _hasher.ComputeCanonicalHash(second);
Assert.Equal(firstHash, secondHash);
}
@@ -37,6 +46,7 @@ public class CanonicalJsonHasherTests
public void AuditEntry_UsesCanonicalHash()
{
var entry = AuditEntry.Create(
hasher: _hasher,
tenantId: "tenant-1",
eventType: AuditEventType.JobCreated,
resourceType: "job",
@@ -45,10 +55,10 @@ public class CanonicalJsonHasherTests
actorType: ActorType.User,
description: "created job");
Assert.True(entry.VerifyIntegrity());
Assert.True(entry.VerifyIntegrity(_hasher));
// Changing description should invalidate hash
var tampered = entry with { Description = "tampered" };
Assert.False(tampered.VerifyIntegrity());
Assert.False(tampered.VerifyIntegrity(_hasher));
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Text.Json;
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core;
using StellaOps.Orchestrator.Core.Hashing;
@@ -7,6 +8,15 @@ namespace StellaOps.Orchestrator.Tests;
public class EventEnvelopeTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
private readonly CanonicalJsonHasher _hasher;
private readonly EventEnvelopeHasher _envelopeHasher;
public EventEnvelopeTests()
{
_hasher = new CanonicalJsonHasher(_cryptoHash);
_envelopeHasher = new EventEnvelopeHasher(_hasher);
}
[Fact]
public void ComputeIdempotencyKey_IsDeterministicAndLowercase()
{
@@ -83,8 +93,8 @@ public class EventEnvelopeTests
eventId: "evt-fixed",
idempotencyKey: "fixed-key");
var hash1 = EventEnvelopeHasher.Compute(envelope);
var hash2 = EventEnvelopeHasher.Compute(envelope);
var hash1 = _envelopeHasher.Compute(envelope);
var hash2 = _envelopeHasher.Compute(envelope);
Assert.Equal(hash1, hash2);
Assert.Equal(64, hash1.Length);

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core.Domain.Events;
using StellaOps.Orchestrator.Infrastructure.Events;
@@ -11,6 +12,7 @@ namespace StellaOps.Orchestrator.Tests.Events;
public class EventPublishingTests
{
private static readonly CancellationToken CT = CancellationToken.None;
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
#region EventEnvelope Tests
@@ -144,7 +146,7 @@ public class EventPublishingTests
tenantId: "tenant-1",
actor: actor);
var digest = envelope.ComputeDigest();
var digest = envelope.ComputeDigest(_cryptoHash);
Assert.StartsWith("sha256:", digest);
Assert.Equal(64 + 7, digest.Length); // "sha256:" + 64 hex chars

View File

@@ -1,3 +1,4 @@
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core.Domain;
using StellaOps.Orchestrator.WebService.Contracts;
@@ -5,6 +6,7 @@ namespace StellaOps.Orchestrator.Tests.PackRun;
public sealed class PackRunContractTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
[Fact]
public void PackRunResponse_FromDomain_MapsAllFields()
{
@@ -88,6 +90,7 @@ public sealed class PackRunContractTests
var now = DateTimeOffset.UtcNow;
var log = PackRunLog.Create(
cryptoHash: _cryptoHash,
packRunId: packRunId,
tenantId: "tenant-1",
sequence: 42,
@@ -121,6 +124,7 @@ public sealed class PackRunContractTests
public void LogEntryResponse_FromDomain_LevelIsLowercase(LogLevel level, string expectedLevelString)
{
var log = PackRunLog.Create(
cryptoHash: _cryptoHash,
packRunId: Guid.NewGuid(),
tenantId: "t1",
sequence: 0,

View File

@@ -1,3 +1,4 @@
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core.Domain;
namespace StellaOps.Orchestrator.Tests.PackRun;
@@ -6,6 +7,7 @@ public sealed class PackRunLogTests
{
private const string TestTenantId = "tenant-test";
private readonly Guid _packRunId = Guid.NewGuid();
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
[Fact]
public void Create_InitializesAllFields()
@@ -13,6 +15,7 @@ public sealed class PackRunLogTests
var now = DateTimeOffset.UtcNow;
var log = PackRunLog.Create(
cryptoHash: _cryptoHash,
packRunId: _packRunId,
tenantId: TestTenantId,
sequence: 5,
@@ -41,6 +44,7 @@ public sealed class PackRunLogTests
var beforeCreate = DateTimeOffset.UtcNow;
var log = PackRunLog.Create(
cryptoHash: _cryptoHash,
packRunId: _packRunId,
tenantId: TestTenantId,
sequence: 0,
@@ -59,7 +63,7 @@ public sealed class PackRunLogTests
{
var now = DateTimeOffset.UtcNow;
var log = PackRunLog.Stdout(_packRunId, TestTenantId, 10, "Hello stdout", now);
var log = PackRunLog.Stdout(_cryptoHash, _packRunId, TestTenantId, 10, "Hello stdout", now);
Assert.Equal(LogLevel.Info, log.Level);
Assert.Equal("stdout", log.Source);
@@ -73,7 +77,7 @@ public sealed class PackRunLogTests
{
var now = DateTimeOffset.UtcNow;
var log = PackRunLog.Stderr(_packRunId, TestTenantId, 20, "Warning message", now);
var log = PackRunLog.Stderr(_cryptoHash, _packRunId, TestTenantId, 20, "Warning message", now);
Assert.Equal(LogLevel.Warn, log.Level);
Assert.Equal("stderr", log.Source);
@@ -86,7 +90,7 @@ public sealed class PackRunLogTests
{
var now = DateTimeOffset.UtcNow;
var log = PackRunLog.System(_packRunId, TestTenantId, 30, LogLevel.Error, "System error", "{\"code\":500}", now);
var log = PackRunLog.System(_cryptoHash, _packRunId, TestTenantId, 30, LogLevel.Error, "System error", "{\"code\":500}", now);
Assert.Equal(LogLevel.Error, log.Level);
Assert.Equal("system", log.Source);
@@ -112,6 +116,7 @@ public sealed class PackRunLogBatchTests
{
private const string TestTenantId = "tenant-test";
private readonly Guid _packRunId = Guid.NewGuid();
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
[Fact]
public void FromLogs_EmptyList_ReturnsEmptyBatch()
@@ -130,9 +135,9 @@ public sealed class PackRunLogBatchTests
{
var logs = new List<PackRunLog>
{
PackRunLog.Create(_packRunId, TestTenantId, 5, LogLevel.Info, "src", "msg1"),
PackRunLog.Create(_packRunId, TestTenantId, 6, LogLevel.Info, "src", "msg2"),
PackRunLog.Create(_packRunId, TestTenantId, 7, LogLevel.Info, "src", "msg3")
PackRunLog.Create(_cryptoHash, _packRunId, TestTenantId, 5, LogLevel.Info, "src", "msg1"),
PackRunLog.Create(_cryptoHash, _packRunId, TestTenantId, 6, LogLevel.Info, "src", "msg2"),
PackRunLog.Create(_cryptoHash, _packRunId, TestTenantId, 7, LogLevel.Info, "src", "msg3")
};
var batch = PackRunLogBatch.FromLogs(_packRunId, TestTenantId, logs);
@@ -151,8 +156,8 @@ public sealed class PackRunLogBatchTests
StartSequence: 100,
Logs:
[
PackRunLog.Create(_packRunId, TestTenantId, 100, LogLevel.Info, "src", "msg1"),
PackRunLog.Create(_packRunId, TestTenantId, 101, LogLevel.Info, "src", "msg2")
PackRunLog.Create(_cryptoHash, _packRunId, TestTenantId, 100, LogLevel.Info, "src", "msg1"),
PackRunLog.Create(_cryptoHash, _packRunId, TestTenantId, 101, LogLevel.Info, "src", "msg2")
]);
Assert.Equal(102, batch.NextSequence);

View File

@@ -1,10 +1,20 @@
using System.Collections.Immutable;
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core.Domain.Replay;
using StellaOps.Orchestrator.Core.Hashing;
namespace StellaOps.Orchestrator.Tests;
public class ReplayInputsLockTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
private readonly CanonicalJsonHasher _hasher;
public ReplayInputsLockTests()
{
_hasher = new CanonicalJsonHasher(_cryptoHash);
}
[Fact]
public void ReplayInputsLock_ComputesStableHash()
{
@@ -22,10 +32,10 @@ public class ReplayInputsLockTests
artifacts: null,
createdAt: new DateTimeOffset(2025, 01, 01, 0, 0, 0, TimeSpan.Zero));
var lock1 = ReplayInputsLock.Create(manifest, createdAt: new DateTimeOffset(2025, 01, 01, 0, 0, 5, TimeSpan.Zero));
var lock2 = ReplayInputsLock.Create(manifest, createdAt: new DateTimeOffset(2025, 01, 01, 0, 0, 5, TimeSpan.Zero));
var lock1 = ReplayInputsLock.Create(manifest, _hasher, createdAt: new DateTimeOffset(2025, 01, 01, 0, 0, 5, TimeSpan.Zero));
var lock2 = ReplayInputsLock.Create(manifest, _hasher, createdAt: new DateTimeOffset(2025, 01, 01, 0, 0, 5, TimeSpan.Zero));
Assert.Equal(lock1.ComputeHash(), lock2.ComputeHash());
Assert.Equal(lock1.ComputeHash(_hasher), lock2.ComputeHash(_hasher));
}
[Fact]
@@ -43,8 +53,8 @@ public class ReplayInputsLockTests
TimeSource: ReplayTimeSource.wall,
Env: ImmutableDictionary<string, string>.Empty));
var inputsLock = ReplayInputsLock.Create(manifest);
var inputsLock = ReplayInputsLock.Create(manifest, _hasher);
Assert.Equal(manifest.ComputeHash(), inputsLock.ManifestHash);
Assert.Equal(manifest.ComputeHash(_hasher), inputsLock.ManifestHash);
}
}

View File

@@ -1,10 +1,20 @@
using System.Collections.Immutable;
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core.Domain.Replay;
using StellaOps.Orchestrator.Core.Hashing;
namespace StellaOps.Orchestrator.Tests;
public class ReplayManifestTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
private readonly CanonicalJsonHasher _hasher;
public ReplayManifestTests()
{
_hasher = new CanonicalJsonHasher(_cryptoHash);
}
[Fact]
public void ComputeHash_IsStableWithCanonicalOrdering()
{
@@ -31,8 +41,8 @@ public class ReplayManifestTests
artifacts: new[] { new ReplayArtifact("ledger.ndjson", "sha256:abc", "application/x-ndjson") },
createdAt: new DateTimeOffset(2025, 12, 1, 0, 0, 0, TimeSpan.Zero));
var hashA = manifestA.ComputeHash();
var hashB = manifestB.ComputeHash();
var hashA = manifestA.ComputeHash(_hasher);
var hashB = manifestB.ComputeHash(_hasher);
Assert.Equal(hashA, hashB);
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Text.Json;
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core;
using StellaOps.Orchestrator.Core.Hashing;
@@ -7,6 +8,14 @@ namespace StellaOps.Orchestrator.Tests;
public class SchemaSmokeTests
{
private readonly ICryptoHash _cryptoHash = DefaultCryptoHash.CreateForTests();
private readonly CanonicalJsonHasher _hasher;
public SchemaSmokeTests()
{
_hasher = new CanonicalJsonHasher(_cryptoHash);
}
[Theory]
[InlineData("event-envelope.schema.json")]
[InlineData("audit-bundle.schema.json")]
@@ -47,8 +56,8 @@ public class SchemaSmokeTests
eventId: "evt-1",
idempotencyKey: "fixed");
var hash1 = CanonicalJsonHasher.ComputeCanonicalSha256(envelope);
var hash2 = CanonicalJsonHasher.ComputeCanonicalSha256(envelope);
var hash1 = _hasher.ComputeCanonicalHash(envelope);
var hash2 = _hasher.ComputeCanonicalHash(envelope);
Assert.Equal(hash1, hash2);
Assert.Equal(64, hash1.Length);

View File

@@ -53,27 +53,27 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="xunit.v3" Version="3.0.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="xunit.v3" Version="3.0.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3"/>
</ItemGroup>
@@ -117,12 +117,14 @@
<ProjectReference Include="..\StellaOps.Orchestrator.Core\StellaOps.Orchestrator.Core.csproj"/>
<ProjectReference Include="..\StellaOps.Orchestrator.Infrastructure\StellaOps.Orchestrator.Infrastructure.csproj"/>
<ProjectReference Include="..\..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj"/>

View File

@@ -1,8 +1,8 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Cryptography;
using StellaOps.Orchestrator.Core.Domain;
using StellaOps.Orchestrator.Core.Domain.Events;
using StellaOps.Orchestrator.Infrastructure;
@@ -102,6 +102,7 @@ public static class PackRunEndpoints
[FromServices] IPackRunRepository packRunRepository,
[FromServices] IQuotaRepository quotaRepository,
[FromServices] IEventPublisher eventPublisher,
[FromServices] ICryptoHash cryptoHash,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken)
{
@@ -127,7 +128,7 @@ public static class PackRunEndpoints
var tenantId = tenantResolver.Resolve(context);
var now = timeProvider.GetUtcNow();
var parameters = request.Parameters ?? "{}";
var parametersDigest = ComputeDigest(parameters);
var parametersDigest = ComputeDigest(cryptoHash, parameters);
var idempotencyKey = request.IdempotencyKey ?? $"pack-run:{request.PackId}:{parametersDigest}:{now:yyyyMMddHHmm}";
// Check for existing pack run with same idempotency key
@@ -429,6 +430,7 @@ public static class PackRunEndpoints
[FromServices] IPackRunRepository packRunRepository,
[FromServices] IPackRunLogRepository logRepository,
[FromServices] IEventPublisher eventPublisher,
[FromServices] ICryptoHash cryptoHash,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken)
{
@@ -471,7 +473,7 @@ public static class PackRunEndpoints
cancellationToken);
// Append system log entry
var log = PackRunLog.System(packRunId, tenantId, 0, PackLogLevel.Info, "Pack run started", null, now);
var log = PackRunLog.System(cryptoHash, packRunId, tenantId, 0, PackLogLevel.Info, "Pack run started", null, now);
await logRepository.AppendAsync(log, cancellationToken);
OrchestratorMetrics.PackRunStarted(tenantId, packRun.PackId);
@@ -499,6 +501,7 @@ public static class PackRunEndpoints
[FromServices] IQuotaRepository quotaRepository,
[FromServices] IArtifactRepository artifactRepository,
[FromServices] IEventPublisher eventPublisher,
[FromServices] ICryptoHash cryptoHash,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken)
{
@@ -587,7 +590,7 @@ public static class PackRunEndpoints
// Append system log entry
var (logCount, latestSeq) = await logRepository.GetLogStatsAsync(tenantId, packRunId, cancellationToken);
var completionLog = PackRunLog.System(
packRunId, tenantId, latestSeq + 1,
cryptoHash, packRunId, tenantId, latestSeq + 1,
request.Success ? PackLogLevel.Info : PackLogLevel.Error,
$"Pack run {(request.Success ? "succeeded" : "failed")} with exit code {request.ExitCode}",
null, now);
@@ -649,6 +652,7 @@ public static class PackRunEndpoints
[FromServices] IPackRunRepository packRunRepository,
[FromServices] IPackRunLogRepository logRepository,
[FromServices] IEventPublisher eventPublisher,
[FromServices] ICryptoHash cryptoHash,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken)
{
@@ -687,7 +691,7 @@ public static class PackRunEndpoints
: PackLogLevel.Info;
logs.Add(PackRunLog.Create(
packRunId, tenantId, seq, level,
cryptoHash, packRunId, tenantId, seq, level,
entry.Source,
entry.Message,
entry.Data,
@@ -773,6 +777,7 @@ public static class PackRunEndpoints
[FromServices] IPackRunLogRepository logRepository,
[FromServices] IQuotaRepository quotaRepository,
[FromServices] IEventPublisher eventPublisher,
[FromServices] ICryptoHash cryptoHash,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken)
{
@@ -811,7 +816,7 @@ public static class PackRunEndpoints
// Append system log entry
var (_, latestSeq) = await logRepository.GetLogStatsAsync(tenantId, packRunId, cancellationToken);
var cancelLog = PackRunLog.System(
packRunId, tenantId, latestSeq + 1,
cryptoHash, packRunId, tenantId, latestSeq + 1,
PackLogLevel.Warn, $"Pack run canceled: {request.Reason}", null, now);
await logRepository.AppendAsync(cancelLog, cancellationToken);
@@ -839,6 +844,7 @@ public static class PackRunEndpoints
[FromServices] TenantResolver tenantResolver,
[FromServices] IPackRunRepository packRunRepository,
[FromServices] IEventPublisher eventPublisher,
[FromServices] ICryptoHash cryptoHash,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken)
{
@@ -868,7 +874,7 @@ public static class PackRunEndpoints
var now = timeProvider.GetUtcNow();
var newPackRunId = Guid.NewGuid();
var parameters = request.Parameters ?? packRun.Parameters;
var parametersDigest = request.Parameters != null ? ComputeDigest(parameters) : packRun.ParametersDigest;
var parametersDigest = request.Parameters != null ? ComputeDigest(cryptoHash, parameters) : packRun.ParametersDigest;
var idempotencyKey = request.IdempotencyKey ?? $"retry:{packRunId}:{now:yyyyMMddHHmmss}";
var newPackRun = PackRun.Create(
@@ -1024,11 +1030,10 @@ public static class PackRunEndpoints
return quota;
}
private static string ComputeDigest(string content)
private static string ComputeDigest(ICryptoHash cryptoHash, string content)
{
var bytes = Encoding.UTF8.GetBytes(content);
var hash = SHA256.HashData(bytes);
return Convert.ToHexStringLower(hash);
return cryptoHash.ComputeHashHexForPurpose(bytes, HashPurpose.Content);
}
private static JsonElement? ToPayload<T>(T value)