Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
namespace StellaOps.Messaging.Testing.Builders;
|
||||
|
||||
/// <summary>
|
||||
/// Builder for creating test messages with customizable properties.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMessage">The message type.</typeparam>
|
||||
public sealed class TestMessageBuilder<TMessage> where TMessage : class, new()
|
||||
{
|
||||
private readonly Dictionary<string, object?> _properties = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sets a property on the message.
|
||||
/// </summary>
|
||||
public TestMessageBuilder<TMessage> With(string propertyName, object? value)
|
||||
{
|
||||
_properties[propertyName] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the message with all configured properties.
|
||||
/// </summary>
|
||||
public TMessage Build()
|
||||
{
|
||||
var message = new TMessage();
|
||||
var type = typeof(TMessage);
|
||||
|
||||
foreach (var (propertyName, value) in _properties)
|
||||
{
|
||||
var property = type.GetProperty(propertyName);
|
||||
if (property is not null && property.CanWrite)
|
||||
{
|
||||
property.SetValue(message, value);
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple test message for queue testing.
|
||||
/// </summary>
|
||||
public sealed record TestQueueMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the message ID.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the message content.
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the priority.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tenant ID.
|
||||
/// </summary>
|
||||
public string? TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets when the message was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets custom metadata.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Metadata { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for creating test messages.
|
||||
/// </summary>
|
||||
public static class TestMessageExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new test queue message with the specified content.
|
||||
/// </summary>
|
||||
public static TestQueueMessage CreateTestMessage(this string content, string? tenantId = null)
|
||||
{
|
||||
return new TestQueueMessage
|
||||
{
|
||||
Content = content,
|
||||
TenantId = tenantId
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates multiple test messages for batch testing.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<TestQueueMessage> CreateTestMessages(int count, string? tenantId = null)
|
||||
{
|
||||
return Enumerable.Range(1, count)
|
||||
.Select(i => new TestQueueMessage
|
||||
{
|
||||
Content = $"Test message {i}",
|
||||
TenantId = tenantId,
|
||||
Priority = i % 3
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Messaging.Abstractions;
|
||||
using StellaOps.Messaging.Transport.InMemory;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Messaging.Testing.Fixtures;
|
||||
|
||||
/// <summary>
|
||||
/// xUnit fixture for in-memory messaging transport.
|
||||
/// Provides fast, isolated test infrastructure without external dependencies.
|
||||
/// </summary>
|
||||
public sealed class InMemoryMessagingFixture : IAsyncLifetime
|
||||
{
|
||||
private InMemoryQueueRegistry _registry = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared queue registry for test coordination.
|
||||
/// </summary>
|
||||
public InMemoryQueueRegistry Registry => _registry;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask InitializeAsync()
|
||||
{
|
||||
_registry = new InMemoryQueueRegistry();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
_registry.Clear();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a service provider configured with in-memory transport.
|
||||
/// </summary>
|
||||
/// <returns>A configured service provider.</returns>
|
||||
public IServiceProvider CreateServiceProvider()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddSingleton(_registry);
|
||||
services.AddSingleton<IMessageQueueFactory, InMemoryMessageQueueFactory>();
|
||||
services.AddSingleton<IDistributedCacheFactory, InMemoryCacheFactory>();
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a message queue factory using in-memory transport.
|
||||
/// </summary>
|
||||
public IMessageQueueFactory CreateQueueFactory()
|
||||
{
|
||||
var sp = CreateServiceProvider();
|
||||
return sp.GetRequiredService<IMessageQueueFactory>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache factory using in-memory transport.
|
||||
/// </summary>
|
||||
public IDistributedCacheFactory CreateCacheFactory()
|
||||
{
|
||||
var sp = CreateServiceProvider();
|
||||
return sp.GetRequiredService<IDistributedCacheFactory>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all queues and caches in the registry.
|
||||
/// Call this between tests for isolation.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_registry.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection definition for in-memory messaging fixture sharing across test classes.
|
||||
/// </summary>
|
||||
[CollectionDefinition(nameof(InMemoryMessagingFixtureCollection))]
|
||||
public class InMemoryMessagingFixtureCollection : ICollectionFixture<InMemoryMessagingFixture>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Messaging.Abstractions;
|
||||
using StellaOps.Messaging.Transport.Postgres;
|
||||
using Testcontainers.PostgreSql;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Messaging.Testing.Fixtures;
|
||||
|
||||
/// <summary>
|
||||
/// xUnit fixture for PostgreSQL testcontainer.
|
||||
/// Provides a containerized PostgreSQL instance for queue integration tests.
|
||||
/// </summary>
|
||||
public sealed class PostgresQueueFixture : IAsyncLifetime
|
||||
{
|
||||
private readonly PostgreSqlContainer _container;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection string for the PostgreSQL instance.
|
||||
/// </summary>
|
||||
public string ConnectionString => _container.GetConnectionString();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostgresQueueFixture"/> class.
|
||||
/// </summary>
|
||||
public PostgresQueueFixture()
|
||||
{
|
||||
_container = new PostgreSqlBuilder()
|
||||
.WithImage("postgres:16-alpine")
|
||||
.WithDatabase("stellaops_messaging_test")
|
||||
.WithUsername("test")
|
||||
.WithPassword("test")
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _container.StartAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _container.StopAsync();
|
||||
await _container.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a service provider configured with PostgreSQL transport.
|
||||
/// </summary>
|
||||
/// <param name="configureOptions">Optional configuration for PostgreSQL options.</param>
|
||||
/// <returns>A configured service provider.</returns>
|
||||
public IServiceProvider CreateServiceProvider(Action<PostgresTransportOptions>? configureOptions = null)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddOptions<PostgresTransportOptions>().Configure(options =>
|
||||
{
|
||||
options.ConnectionString = ConnectionString;
|
||||
options.Schema = "messaging";
|
||||
options.AutoCreateTables = true;
|
||||
configureOptions?.Invoke(options);
|
||||
});
|
||||
|
||||
services.AddSingleton<PostgresConnectionFactory>();
|
||||
services.AddSingleton<IMessageQueueFactory, PostgresMessageQueueFactory>();
|
||||
services.AddSingleton<IDistributedCacheFactory, PostgresCacheFactory>();
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a message queue factory using PostgreSQL transport.
|
||||
/// </summary>
|
||||
public IMessageQueueFactory CreateQueueFactory()
|
||||
{
|
||||
var sp = CreateServiceProvider();
|
||||
return sp.GetRequiredService<IMessageQueueFactory>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache factory using PostgreSQL transport.
|
||||
/// </summary>
|
||||
public IDistributedCacheFactory CreateCacheFactory()
|
||||
{
|
||||
var sp = CreateServiceProvider();
|
||||
return sp.GetRequiredService<IDistributedCacheFactory>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection definition for PostgreSQL fixture sharing across test classes.
|
||||
/// </summary>
|
||||
[CollectionDefinition(nameof(PostgresQueueFixtureCollection))]
|
||||
public class PostgresQueueFixtureCollection : ICollectionFixture<PostgresQueueFixture>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using DotNet.Testcontainers.Builders;
|
||||
using DotNet.Testcontainers.Containers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Messaging.Abstractions;
|
||||
using StellaOps.Messaging.Transport.Valkey;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Messaging.Testing.Fixtures;
|
||||
|
||||
/// <summary>
|
||||
/// xUnit fixture for Valkey testcontainer.
|
||||
/// Provides a containerized Valkey instance for integration tests.
|
||||
/// </summary>
|
||||
public sealed class ValkeyFixture : IAsyncLifetime
|
||||
{
|
||||
private readonly IContainer _container;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection string for the Valkey instance.
|
||||
/// </summary>
|
||||
public string ConnectionString { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the host of the Valkey instance.
|
||||
/// </summary>
|
||||
public string Host => _container.Hostname;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mapped port for the Valkey instance.
|
||||
/// </summary>
|
||||
public ushort Port { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ValkeyFixture"/> class.
|
||||
/// </summary>
|
||||
public ValkeyFixture()
|
||||
{
|
||||
_container = new ContainerBuilder()
|
||||
.WithImage("valkey/valkey:8-alpine")
|
||||
.WithPortBinding(6379, true)
|
||||
.WithWaitStrategy(Wait.ForUnixContainer()
|
||||
.UntilCommandIsCompleted("valkey-cli", "ping"))
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _container.StartAsync();
|
||||
Port = _container.GetMappedPublicPort(6379);
|
||||
ConnectionString = $"{Host}:{Port}";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _container.StopAsync();
|
||||
await _container.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a service provider configured with Valkey transport.
|
||||
/// </summary>
|
||||
/// <param name="configureOptions">Optional configuration for Valkey options.</param>
|
||||
/// <returns>A configured service provider.</returns>
|
||||
public IServiceProvider CreateServiceProvider(Action<ValkeyTransportOptions>? configureOptions = null)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddOptions<ValkeyTransportOptions>().Configure(options =>
|
||||
{
|
||||
options.ConnectionString = ConnectionString;
|
||||
configureOptions?.Invoke(options);
|
||||
});
|
||||
|
||||
services.AddSingleton<ValkeyConnectionFactory>();
|
||||
services.AddSingleton<IMessageQueueFactory, ValkeyMessageQueueFactory>();
|
||||
services.AddSingleton<IDistributedCacheFactory, ValkeyCacheFactory>();
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a message queue factory using Valkey transport.
|
||||
/// </summary>
|
||||
public IMessageQueueFactory CreateQueueFactory()
|
||||
{
|
||||
var sp = CreateServiceProvider();
|
||||
return sp.GetRequiredService<IMessageQueueFactory>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache factory using Valkey transport.
|
||||
/// </summary>
|
||||
public IDistributedCacheFactory CreateCacheFactory()
|
||||
{
|
||||
var sp = CreateServiceProvider();
|
||||
return sp.GetRequiredService<IDistributedCacheFactory>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection definition for Valkey fixture sharing across test classes.
|
||||
/// </summary>
|
||||
[CollectionDefinition(nameof(ValkeyFixtureCollection))]
|
||||
public class ValkeyFixtureCollection : ICollectionFixture<ValkeyFixture>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<UseXunitV3>true</UseXunitV3>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Messaging.Testing</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Messaging.Transport.Valkey\StellaOps.Messaging.Transport.Valkey.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Messaging.Transport.Postgres\StellaOps.Messaging.Transport.Postgres.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Messaging.Transport.InMemory\StellaOps.Messaging.Transport.InMemory.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Testcontainers" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" />
|
||||
<PackageReference Include="xunit.v3.extensibility.core" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,210 @@
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Frames;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
namespace StellaOps.Router.Testing.Factories;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating test frames with sensible defaults.
|
||||
/// </summary>
|
||||
public static class TestFrameFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a request frame with the specified payload.
|
||||
/// </summary>
|
||||
public static Frame CreateRequestFrame(
|
||||
byte[]? payload = null,
|
||||
string? correlationId = null,
|
||||
FrameType frameType = FrameType.Request)
|
||||
{
|
||||
return new Frame
|
||||
{
|
||||
Type = frameType,
|
||||
CorrelationId = correlationId ?? Guid.NewGuid().ToString("N"),
|
||||
Payload = payload ?? Array.Empty<byte>()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a response frame for the given correlation ID.
|
||||
/// </summary>
|
||||
public static Frame CreateResponseFrame(
|
||||
string correlationId,
|
||||
byte[]? payload = null)
|
||||
{
|
||||
return new Frame
|
||||
{
|
||||
Type = FrameType.Response,
|
||||
CorrelationId = correlationId,
|
||||
Payload = payload ?? Array.Empty<byte>()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a hello frame for service registration.
|
||||
/// </summary>
|
||||
public static Frame CreateHelloFrame(
|
||||
string serviceName = "test-service",
|
||||
string version = "1.0.0",
|
||||
string region = "test",
|
||||
string instanceId = "test-instance",
|
||||
IReadOnlyList<EndpointDescriptor>? endpoints = null)
|
||||
{
|
||||
var helloPayload = new HelloPayload
|
||||
{
|
||||
Instance = new InstanceDescriptor
|
||||
{
|
||||
InstanceId = instanceId,
|
||||
ServiceName = serviceName,
|
||||
Version = version,
|
||||
Region = region
|
||||
},
|
||||
Endpoints = endpoints ?? []
|
||||
};
|
||||
|
||||
return new Frame
|
||||
{
|
||||
Type = FrameType.Hello,
|
||||
CorrelationId = Guid.NewGuid().ToString("N"),
|
||||
Payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(helloPayload)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a heartbeat frame.
|
||||
/// </summary>
|
||||
public static Frame CreateHeartbeatFrame(
|
||||
string instanceId = "test-instance",
|
||||
InstanceHealthStatus status = InstanceHealthStatus.Healthy,
|
||||
int inFlightRequestCount = 0,
|
||||
double errorRate = 0.0)
|
||||
{
|
||||
var heartbeatPayload = new HeartbeatPayload
|
||||
{
|
||||
InstanceId = instanceId,
|
||||
Status = status,
|
||||
InFlightRequestCount = inFlightRequestCount,
|
||||
ErrorRate = errorRate,
|
||||
TimestampUtc = DateTime.UtcNow
|
||||
};
|
||||
|
||||
return new Frame
|
||||
{
|
||||
Type = FrameType.Heartbeat,
|
||||
CorrelationId = Guid.NewGuid().ToString("N"),
|
||||
Payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(heartbeatPayload)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cancel frame for the given correlation ID.
|
||||
/// </summary>
|
||||
public static Frame CreateCancelFrame(
|
||||
string correlationId,
|
||||
string? reason = null)
|
||||
{
|
||||
var cancelPayload = new CancelPayload
|
||||
{
|
||||
Reason = reason ?? CancelReasons.Timeout
|
||||
};
|
||||
|
||||
return new Frame
|
||||
{
|
||||
Type = FrameType.Cancel,
|
||||
CorrelationId = correlationId,
|
||||
Payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(cancelPayload)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a frame with a specific payload size for testing limits.
|
||||
/// </summary>
|
||||
public static Frame CreateFrameWithPayloadSize(int payloadSize)
|
||||
{
|
||||
var payload = new byte[payloadSize];
|
||||
Random.Shared.NextBytes(payload);
|
||||
|
||||
return new Frame
|
||||
{
|
||||
Type = FrameType.Request,
|
||||
CorrelationId = Guid.NewGuid().ToString("N"),
|
||||
Payload = payload
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a request frame from JSON content.
|
||||
/// </summary>
|
||||
public static RequestFrame CreateTypedRequestFrame<T>(
|
||||
T request,
|
||||
string method = "POST",
|
||||
string path = "/test",
|
||||
Dictionary<string, string>? headers = null)
|
||||
{
|
||||
return new RequestFrame
|
||||
{
|
||||
RequestId = Guid.NewGuid().ToString("N"),
|
||||
CorrelationId = Guid.NewGuid().ToString("N"),
|
||||
Method = method,
|
||||
Path = path,
|
||||
Headers = headers ?? new Dictionary<string, string>(),
|
||||
Payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(request)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an endpoint descriptor for testing.
|
||||
/// </summary>
|
||||
public static EndpointDescriptor CreateEndpointDescriptor(
|
||||
string method = "GET",
|
||||
string path = "/test",
|
||||
string serviceName = "test-service",
|
||||
string version = "1.0.0",
|
||||
int timeoutSeconds = 30,
|
||||
bool supportsStreaming = false,
|
||||
IReadOnlyList<ClaimRequirement>? requiringClaims = null)
|
||||
{
|
||||
return new EndpointDescriptor
|
||||
{
|
||||
Method = method,
|
||||
Path = path,
|
||||
ServiceName = serviceName,
|
||||
Version = version,
|
||||
DefaultTimeout = TimeSpan.FromSeconds(timeoutSeconds),
|
||||
SupportsStreaming = supportsStreaming,
|
||||
RequiringClaims = requiringClaims ?? []
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance descriptor for testing.
|
||||
/// </summary>
|
||||
public static InstanceDescriptor CreateInstanceDescriptor(
|
||||
string instanceId = "test-instance",
|
||||
string serviceName = "test-service",
|
||||
string version = "1.0.0",
|
||||
string region = "test")
|
||||
{
|
||||
return new InstanceDescriptor
|
||||
{
|
||||
InstanceId = instanceId,
|
||||
ServiceName = serviceName,
|
||||
Version = version,
|
||||
Region = region
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a claim requirement for testing.
|
||||
/// </summary>
|
||||
public static ClaimRequirement CreateClaimRequirement(
|
||||
string type,
|
||||
string? value = null)
|
||||
{
|
||||
return new ClaimRequirement
|
||||
{
|
||||
Type = type,
|
||||
Value = value
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Router.Testing.Fixtures;
|
||||
|
||||
/// <summary>
|
||||
/// Base test fixture for Router tests providing common utilities.
|
||||
/// Implements IAsyncLifetime for async setup/teardown.
|
||||
/// </summary>
|
||||
public abstract class RouterTestFixture : IAsyncLifetime
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a null logger factory for tests that don't need logging.
|
||||
/// </summary>
|
||||
protected ILoggerFactory LoggerFactory { get; } = NullLoggerFactory.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a null logger for tests that don't need logging.
|
||||
/// </summary>
|
||||
protected ILogger<T> GetLogger<T>() => NullLogger<T>.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cancellation token that times out after the specified duration.
|
||||
/// </summary>
|
||||
protected static CancellationToken CreateTimeoutToken(TimeSpan timeout)
|
||||
{
|
||||
var cts = new CancellationTokenSource(timeout);
|
||||
return cts.Token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cancellation token that times out after 5 seconds (default for tests).
|
||||
/// </summary>
|
||||
protected static CancellationToken CreateTestTimeoutToken()
|
||||
{
|
||||
return CreateTimeoutToken(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for a condition to be true with timeout.
|
||||
/// </summary>
|
||||
protected static async Task WaitForConditionAsync(
|
||||
Func<bool> condition,
|
||||
TimeSpan timeout,
|
||||
TimeSpan? pollInterval = null)
|
||||
{
|
||||
var interval = pollInterval ?? TimeSpan.FromMilliseconds(50);
|
||||
var deadline = DateTimeOffset.UtcNow + timeout;
|
||||
|
||||
while (DateTimeOffset.UtcNow < deadline)
|
||||
{
|
||||
if (condition())
|
||||
return;
|
||||
|
||||
await Task.Delay(interval);
|
||||
}
|
||||
|
||||
throw new TimeoutException($"Condition not met within {timeout}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for an async condition to be true with timeout.
|
||||
/// </summary>
|
||||
protected static async Task WaitForConditionAsync(
|
||||
Func<Task<bool>> condition,
|
||||
TimeSpan timeout,
|
||||
TimeSpan? pollInterval = null)
|
||||
{
|
||||
var interval = pollInterval ?? TimeSpan.FromMilliseconds(50);
|
||||
var deadline = DateTimeOffset.UtcNow + timeout;
|
||||
|
||||
while (DateTimeOffset.UtcNow < deadline)
|
||||
{
|
||||
if (await condition())
|
||||
return;
|
||||
|
||||
await Task.Delay(interval);
|
||||
}
|
||||
|
||||
throw new TimeoutException($"Condition not met within {timeout}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override for async initialization.
|
||||
/// </summary>
|
||||
public virtual Task InitializeAsync() => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Override for async cleanup.
|
||||
/// </summary>
|
||||
public virtual Task DisposeAsync() => Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection fixture for sharing state across tests in the same collection.
|
||||
/// </summary>
|
||||
public abstract class RouterCollectionFixture : IAsyncLifetime
|
||||
{
|
||||
public virtual Task InitializeAsync() => Task.CompletedTask;
|
||||
public virtual Task DisposeAsync() => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
namespace StellaOps.Router.Testing.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// A mock connection state for testing routing and connection management.
|
||||
/// </summary>
|
||||
public sealed class MockConnectionState
|
||||
{
|
||||
public string ConnectionId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
public string ServiceName { get; init; } = "test-service";
|
||||
public string Version { get; init; } = "1.0.0";
|
||||
public string Region { get; init; } = "test";
|
||||
public string InstanceId { get; init; } = "test-instance";
|
||||
public InstanceHealthStatus HealthStatus { get; set; } = InstanceHealthStatus.Healthy;
|
||||
public DateTimeOffset ConnectedAtUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset LastHeartbeatUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public int InflightRequests { get; set; }
|
||||
public int Weight { get; set; } = 100;
|
||||
public List<EndpointDescriptor> Endpoints { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a connection state for testing.
|
||||
/// </summary>
|
||||
public static MockConnectionState Create(
|
||||
string? serviceName = null,
|
||||
string? instanceId = null,
|
||||
InstanceHealthStatus status = InstanceHealthStatus.Healthy)
|
||||
{
|
||||
return new MockConnectionState
|
||||
{
|
||||
ServiceName = serviceName ?? "test-service",
|
||||
InstanceId = instanceId ?? $"instance-{Guid.NewGuid():N}",
|
||||
HealthStatus = status
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates multiple connection states simulating a service cluster.
|
||||
/// </summary>
|
||||
public static List<MockConnectionState> CreateCluster(
|
||||
string serviceName,
|
||||
int instanceCount,
|
||||
InstanceHealthStatus status = InstanceHealthStatus.Healthy)
|
||||
{
|
||||
return Enumerable.Range(0, instanceCount)
|
||||
.Select(i => new MockConnectionState
|
||||
{
|
||||
ServiceName = serviceName,
|
||||
InstanceId = $"{serviceName}-{i}",
|
||||
HealthStatus = status
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Router.Testing.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// A logger that records all log entries for assertions.
|
||||
/// </summary>
|
||||
public sealed class RecordingLogger<T> : ILogger<T>
|
||||
{
|
||||
private readonly ConcurrentQueue<LogEntry> _entries = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all recorded log entries.
|
||||
/// </summary>
|
||||
public IReadOnlyList<LogEntry> Entries => _entries.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets entries filtered by log level.
|
||||
/// </summary>
|
||||
public IEnumerable<LogEntry> GetEntries(LogLevel level) =>
|
||||
_entries.Where(e => e.Level == level);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all error entries.
|
||||
/// </summary>
|
||||
public IEnumerable<LogEntry> Errors => GetEntries(LogLevel.Error);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all warning entries.
|
||||
/// </summary>
|
||||
public IEnumerable<LogEntry> Warnings => GetEntries(LogLevel.Warning);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all recorded entries.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
while (_entries.TryDequeue(out _)) { }
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull =>
|
||||
NullScope.Instance;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel,
|
||||
EventId eventId,
|
||||
TState state,
|
||||
Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
_entries.Enqueue(new LogEntry
|
||||
{
|
||||
Level = logLevel,
|
||||
EventId = eventId,
|
||||
Message = formatter(state, exception),
|
||||
Exception = exception,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class NullScope : IDisposable
|
||||
{
|
||||
public static readonly NullScope Instance = new();
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a recorded log entry.
|
||||
/// </summary>
|
||||
public sealed record LogEntry
|
||||
{
|
||||
public required LogLevel Level { get; init; }
|
||||
public required EventId EventId { get; init; }
|
||||
public required string Message { get; init; }
|
||||
public Exception? Exception { get; init; }
|
||||
public DateTimeOffset Timestamp { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A logger factory that creates recording loggers.
|
||||
/// </summary>
|
||||
public sealed class RecordingLoggerFactory : ILoggerFactory
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, object> _loggers = new();
|
||||
|
||||
public ILogger CreateLogger(string categoryName) =>
|
||||
(ILogger)_loggers.GetOrAdd(categoryName, _ => new RecordingLogger<object>());
|
||||
|
||||
public ILogger<T> CreateLogger<T>() =>
|
||||
(ILogger<T>)_loggers.GetOrAdd(typeof(T).FullName!, _ => new RecordingLogger<T>());
|
||||
|
||||
public RecordingLogger<T>? GetLogger<T>() =>
|
||||
_loggers.TryGetValue(typeof(T).FullName!, out var logger)
|
||||
? logger as RecordingLogger<T>
|
||||
: null;
|
||||
|
||||
public void AddProvider(ILoggerProvider provider) { }
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>StellaOps.Router.Testing</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="xunit" PrivateAssets="all" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user