Add Canonical JSON serialization library with tests and documentation

- Implemented CanonJson class for deterministic JSON serialization and hashing.
- Added unit tests for CanonJson functionality, covering various scenarios including key sorting, handling of nested objects, arrays, and special characters.
- Created project files for the Canonical JSON library and its tests, including necessary package references.
- Added README.md for library usage and API reference.
- Introduced RabbitMqIntegrationFactAttribute for conditional RabbitMQ integration tests.
This commit is contained in:
master
2025-12-19 15:35:00 +02:00
parent 43882078a4
commit 951a38d561
192 changed files with 27550 additions and 2611 deletions

View File

@@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Router.Testing.Fixtures;
using Testcontainers.RabbitMq;
using Xunit.Sdk;
namespace StellaOps.Router.Transport.RabbitMq.Tests.Fixtures;
@@ -82,15 +83,37 @@ public sealed class RabbitMqContainerFixture : RouterCollectionFixture, IAsyncDi
/// <inheritdoc />
public override async Task InitializeAsync()
{
_container = new RabbitMqBuilder()
.WithImage("rabbitmq:3.12-management")
.WithPortBinding(5672, true)
.WithPortBinding(15672, true)
.WithUsername("guest")
.WithPassword("guest")
.Build();
try
{
_container = new RabbitMqBuilder()
.WithImage("rabbitmq:3.12-management")
.WithPortBinding(5672, true)
.WithPortBinding(15672, true)
.WithUsername("guest")
.WithPassword("guest")
.Build();
await _container.StartAsync();
await _container.StartAsync();
}
catch (Exception ex)
{
try
{
if (_container is not null)
{
await _container.DisposeAsync();
}
}
catch
{
// Ignore cleanup failures during skip.
}
_container = null;
throw SkipException.ForSkip(
$"RabbitMQ integration tests require Docker/Testcontainers. Skipping because the container failed to start: {ex.Message}");
}
}
/// <inheritdoc />

View File

@@ -0,0 +1,19 @@
using System;
using Xunit;
namespace StellaOps.Router.Transport.RabbitMq.Tests.Fixtures;
[AttributeUsage(AttributeTargets.Method)]
public sealed class RabbitMqIntegrationFactAttribute : FactAttribute
{
public RabbitMqIntegrationFactAttribute()
{
var enabled = Environment.GetEnvironmentVariable("STELLAOPS_TEST_RABBITMQ");
if (!string.Equals(enabled, "1", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(enabled, "true", StringComparison.OrdinalIgnoreCase))
{
Skip = "RabbitMQ integration tests are opt-in. Set STELLAOPS_TEST_RABBITMQ=1 (requires Docker/Testcontainers).";
}
}
}

View File

@@ -60,7 +60,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
#region Connection Tests
[Fact]
[RabbitMqIntegrationFact]
public async Task ServerStartAsync_WithRealBroker_Succeeds()
{
// Arrange
@@ -74,7 +74,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
_server.ConnectionCount.Should().Be(0);
}
[Fact]
[RabbitMqIntegrationFact]
public async Task ServerStopAsync_AfterStart_Succeeds()
{
// Arrange
@@ -88,7 +88,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
await act.Should().NotThrowAsync();
}
[Fact]
[RabbitMqIntegrationFact]
public async Task ClientConnectAsync_WithRealBroker_Succeeds()
{
// Arrange
@@ -108,7 +108,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
await act.Should().NotThrowAsync();
}
[Fact]
[RabbitMqIntegrationFact]
public async Task ClientDisconnectAsync_AfterConnect_Succeeds()
{
// Arrange
@@ -133,7 +133,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
#region Hello Frame Tests
[Fact]
[RabbitMqIntegrationFact]
public async Task ClientConnectAsync_SendsHelloFrame_ServerReceives()
{
// Arrange
@@ -180,7 +180,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
#region Heartbeat Tests
[Fact]
[RabbitMqIntegrationFact]
public async Task ClientSendHeartbeatAsync_RealBroker_Succeeds()
{
// Arrange
@@ -210,7 +210,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
await act.Should().NotThrowAsync();
}
[Fact]
[RabbitMqIntegrationFact]
public async Task ServerReceivesHeartbeat_UpdatesLastHeartbeatUtc()
{
// Arrange
@@ -272,7 +272,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
#region Queue Declaration Tests
[Fact]
[RabbitMqIntegrationFact]
public async Task ServerStartAsync_CreatesExchangesAndQueues()
{
// Arrange
@@ -286,7 +286,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
// but the lack of exception indicates success
}
[Fact]
[RabbitMqIntegrationFact]
public async Task ClientConnectAsync_CreatesResponseQueue()
{
// Arrange
@@ -309,7 +309,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
#region Auto-Delete Queue Tests
[Fact]
[RabbitMqIntegrationFact]
public async Task AutoDeleteQueues_AreCleanedUpOnDisconnect()
{
// Arrange
@@ -343,7 +343,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
#region Prefetch Tests
[Fact]
[RabbitMqIntegrationFact]
public async Task PrefetchCount_IsAppliedOnConnect()
{
// Arrange
@@ -372,7 +372,7 @@ public sealed class RabbitMqIntegrationTests : IAsyncLifetime
#region Multiple Connections Tests
[Fact]
[RabbitMqIntegrationFact]
public async Task MultipleClients_CanConnectSimultaneously()
{
// Arrange

View File

@@ -112,10 +112,10 @@ public sealed class RabbitMqTransportClientTests
#region CancelAllInflight Tests
[Fact]
public void CancelAllInflight_WhenNoInflightRequests_DoesNotThrow()
public async Task CancelAllInflight_WhenNoInflightRequests_DoesNotThrow()
{
// Arrange
using var client = CreateClient();
await using var client = CreateClient();
// Act & Assert - should not throw
client.CancelAllInflight("TestReason");
@@ -373,12 +373,12 @@ public sealed class RabbitMqTransportClientConfigurationTests
// Arrange
var options = new RabbitMqTransportOptions
{
QueuePrefix = "myapp"
ExchangePrefix = "myapp"
};
// Assert
options.RequestExchange.Should().Be("myapp.request");
options.ResponseExchange.Should().Be("myapp.response");
options.RequestExchange.Should().Be("myapp.requests");
options.ResponseExchange.Should().Be("myapp.responses");
}
[Fact]
@@ -388,7 +388,7 @@ public sealed class RabbitMqTransportClientConfigurationTests
var options = new RabbitMqTransportOptions();
// Assert
options.RequestExchange.Should().Be("stellaops.request");
options.ResponseExchange.Should().Be("stellaops.response");
options.RequestExchange.Should().Be("stella.router.requests");
options.ResponseExchange.Should().Be("stella.router.responses");
}
}

View File

@@ -99,10 +99,10 @@ public sealed class RabbitMqTransportServerTests
#region Connection Management Tests
[Fact]
public void GetConnectionState_WithUnknownConnectionId_ReturnsNull()
public async Task GetConnectionState_WithUnknownConnectionId_ReturnsNull()
{
// Arrange
using var server = CreateServer();
await using var server = CreateServer();
// Act
var result = server.GetConnectionState("unknown-connection");
@@ -112,10 +112,10 @@ public sealed class RabbitMqTransportServerTests
}
[Fact]
public void GetConnections_WhenEmpty_ReturnsEmptyEnumerable()
public async Task GetConnections_WhenEmpty_ReturnsEmptyEnumerable()
{
// Arrange
using var server = CreateServer();
await using var server = CreateServer();
// Act
var result = server.GetConnections().ToList();
@@ -125,10 +125,10 @@ public sealed class RabbitMqTransportServerTests
}
[Fact]
public void ConnectionCount_WhenEmpty_ReturnsZero()
public async Task ConnectionCount_WhenEmpty_ReturnsZero()
{
// Arrange
using var server = CreateServer();
await using var server = CreateServer();
// Act
var result = server.ConnectionCount;
@@ -138,10 +138,10 @@ public sealed class RabbitMqTransportServerTests
}
[Fact]
public void RemoveConnection_WithUnknownConnectionId_DoesNotThrow()
public async Task RemoveConnection_WithUnknownConnectionId_DoesNotThrow()
{
// Arrange
using var server = CreateServer();
await using var server = CreateServer();
// Act
var act = () => server.RemoveConnection("unknown-connection");
@@ -155,10 +155,10 @@ public sealed class RabbitMqTransportServerTests
#region Event Handler Tests
[Fact]
public void OnConnection_CanBeRegistered()
public async Task OnConnection_CanBeRegistered()
{
// Arrange
using var server = CreateServer();
await using var server = CreateServer();
var connectionReceived = false;
// Act
@@ -172,10 +172,10 @@ public sealed class RabbitMqTransportServerTests
}
[Fact]
public void OnDisconnection_CanBeRegistered()
public async Task OnDisconnection_CanBeRegistered()
{
// Arrange
using var server = CreateServer();
await using var server = CreateServer();
var disconnectionReceived = false;
// Act
@@ -189,10 +189,10 @@ public sealed class RabbitMqTransportServerTests
}
[Fact]
public void OnFrame_CanBeRegistered()
public async Task OnFrame_CanBeRegistered()
{
// Arrange
using var server = CreateServer();
await using var server = CreateServer();
var frameReceived = false;
// Act
@@ -252,7 +252,7 @@ public sealed class RabbitMqTransportServerTests
public async Task SendFrameAsync_WithUnknownConnection_ThrowsInvalidOperationException()
{
// Arrange
using var server = CreateServer();
await using var server = CreateServer();
var frame = new Frame
{
@@ -277,7 +277,7 @@ public sealed class RabbitMqTransportServerTests
public async Task StopAsync_WhenNotStarted_DoesNotThrow()
{
// Arrange
using var server = CreateServer();
await using var server = CreateServer();
// Act
var act = async () => await server.StopAsync(CancellationToken.None);