Add unit tests for Router configuration and transport layers
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

- Implemented tests for RouterConfig, RoutingOptions, StaticInstanceConfig, and RouterConfigOptions to ensure default values are set correctly.
- Added tests for RouterConfigProvider to validate configurations and ensure defaults are returned when no file is specified.
- Created tests for ConfigValidationResult to check success and error scenarios.
- Developed tests for ServiceCollectionExtensions to verify service registration for RouterConfig.
- Introduced UdpTransportTests to validate serialization, connection, request-response, and error handling in UDP transport.
- Added scripts for signing authority gaps and hashing DevPortal SDK snippets.
This commit is contained in:
StellaOps Bot
2025-12-05 08:01:47 +02:00
parent 635c70e828
commit 6a299d231f
294 changed files with 28434 additions and 1329 deletions

View File

@@ -5,6 +5,7 @@ using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Common.Models;
using static StellaOps.Router.Common.Models.CancelReasons;
namespace StellaOps.Router.Transport.InMemory;
@@ -12,12 +13,13 @@ namespace StellaOps.Router.Transport.InMemory;
/// In-memory transport client implementation for testing and development.
/// Used by the Microservice SDK to send frames to the Gateway.
/// </summary>
public sealed class InMemoryTransportClient : ITransportClient, IDisposable
public sealed class InMemoryTransportClient : ITransportClient, IMicroserviceTransport, IDisposable
{
private readonly InMemoryConnectionRegistry _registry;
private readonly InMemoryTransportOptions _options;
private readonly ILogger<InMemoryTransportClient> _logger;
private readonly ConcurrentDictionary<string, TaskCompletionSource<Frame>> _pendingRequests = new();
private readonly ConcurrentDictionary<string, CancellationTokenSource> _inflightHandlers = new();
private readonly CancellationTokenSource _clientCts = new();
private bool _disposed;
private string? _connectionId;
@@ -172,29 +174,54 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
return;
}
var correlationId = frame.CorrelationId ?? Guid.NewGuid().ToString("N");
// Create a linked CancellationTokenSource for this handler
// This allows cancellation via CANCEL frame or transport shutdown
using var handlerCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_inflightHandlers[correlationId] = handlerCts;
try
{
var response = await OnRequestReceived(frame, cancellationToken);
var response = await OnRequestReceived(frame, handlerCts.Token);
// Ensure response has same correlation ID
var responseFrame = response with { CorrelationId = frame.CorrelationId };
await channel.ToGateway.Writer.WriteAsync(responseFrame, cancellationToken);
var responseFrame = response with { CorrelationId = correlationId };
// Only send response if not cancelled
if (!handlerCts.Token.IsCancellationRequested)
{
await channel.ToGateway.Writer.WriteAsync(responseFrame, cancellationToken);
}
else
{
_logger.LogDebug("Not sending response for cancelled request {CorrelationId}", correlationId);
}
}
catch (OperationCanceledException)
{
_logger.LogDebug("Request {CorrelationId} was cancelled", frame.CorrelationId);
_logger.LogDebug("Request {CorrelationId} was cancelled", correlationId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling request {CorrelationId}", frame.CorrelationId);
// Send error response
var errorFrame = new Frame
_logger.LogError(ex, "Error handling request {CorrelationId}", correlationId);
// Only send error response if not cancelled
if (!handlerCts.Token.IsCancellationRequested)
{
Type = FrameType.Response,
CorrelationId = frame.CorrelationId,
Payload = ReadOnlyMemory<byte>.Empty
};
await channel.ToGateway.Writer.WriteAsync(errorFrame, cancellationToken);
var errorFrame = new Frame
{
Type = FrameType.Response,
CorrelationId = correlationId,
Payload = ReadOnlyMemory<byte>.Empty
};
await channel.ToGateway.Writer.WriteAsync(errorFrame, cancellationToken);
}
}
finally
{
// Remove from inflight tracking
_inflightHandlers.TryRemove(correlationId, out _);
}
}
@@ -204,13 +231,27 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
_logger.LogDebug("Received CANCEL for correlation {CorrelationId}", frame.CorrelationId);
// Cancel the inflight handler via its CancellationTokenSource
if (_inflightHandlers.TryGetValue(frame.CorrelationId, out var handlerCts))
{
try
{
handlerCts.Cancel();
_logger.LogInformation("Cancelled handler for request {CorrelationId}", frame.CorrelationId);
}
catch (ObjectDisposedException)
{
// Handler already completed
}
}
// Complete any pending request with cancellation
if (_pendingRequests.TryRemove(frame.CorrelationId, out var tcs))
{
tcs.TrySetCanceled();
}
// Notify handler
// Notify external handler (for custom cancellation logic)
if (OnCancelReceived is not null && Guid.TryParse(frame.CorrelationId, out var correlationGuid))
{
_ = OnCancelReceived(correlationGuid, null);
@@ -381,6 +422,33 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
await channel.ToGateway.Writer.WriteAsync(frame, cancellationToken);
}
/// <summary>
/// Cancels all in-flight handler requests.
/// Called when connection is closed or transport is shutting down.
/// </summary>
/// <param name="reason">The reason for cancellation.</param>
public void CancelAllInflight(string reason)
{
var count = 0;
foreach (var kvp in _inflightHandlers)
{
try
{
kvp.Value.Cancel();
count++;
}
catch (ObjectDisposedException)
{
// Already completed/disposed
}
}
if (count > 0)
{
_logger.LogInformation("Cancelled {Count} in-flight handlers: {Reason}", count, reason);
}
}
/// <summary>
/// Disconnects from the transport.
/// </summary>
@@ -388,6 +456,9 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
{
if (_connectionId is null) return;
// Cancel all inflight handlers before disconnecting
CancelAllInflight(CancelReasons.Shutdown);
await _clientCts.CancelAsync();
if (_receiveTask is not null)
@@ -407,6 +478,9 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
if (_disposed) return;
_disposed = true;
// Cancel all inflight handlers
CancelAllInflight(Shutdown);
_clientCts.Cancel();
foreach (var tcs in _pendingRequests.Values)
@@ -414,6 +488,7 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
tcs.TrySetCanceled();
}
_pendingRequests.Clear();
_inflightHandlers.Clear();
if (_connectionId is not null)
{

View File

@@ -35,6 +35,7 @@ public static class ServiceCollectionExtensions
// Register interfaces
services.TryAddSingleton<ITransportServer>(sp => sp.GetRequiredService<InMemoryTransportServer>());
services.TryAddSingleton<ITransportClient>(sp => sp.GetRequiredService<InMemoryTransportClient>());
services.TryAddSingleton<IMicroserviceTransport>(sp => sp.GetRequiredService<InMemoryTransportClient>());
return services;
}
@@ -81,6 +82,7 @@ public static class ServiceCollectionExtensions
services.TryAddSingleton<InMemoryConnectionRegistry>();
services.TryAddSingleton<InMemoryTransportClient>();
services.TryAddSingleton<ITransportClient>(sp => sp.GetRequiredService<InMemoryTransportClient>());
services.TryAddSingleton<IMicroserviceTransport>(sp => sp.GetRequiredService<InMemoryTransportClient>());
return services;
}