Add unit tests and logging infrastructure for InMemory and RabbitMQ transports
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented RecordingLogger and RecordingLoggerFactory for capturing log entries in tests. - Added unit tests for InMemoryChannel, covering constructor behavior, property assignments, channel communication, and disposal. - Created InMemoryTransportOptionsTests to validate default values and customizable options for InMemory transport. - Developed RabbitMqFrameProtocolTests to ensure correct parsing and property creation for RabbitMQ frames. - Added RabbitMqTransportOptionsTests to verify default settings and customization options for RabbitMQ transport. - Updated project files for testing libraries and dependencies.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="ConfigChangedEventArgs"/>.
|
||||
/// </summary>
|
||||
public sealed class ConfigChangedEventArgsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPreviousConfig()
|
||||
{
|
||||
// Arrange
|
||||
var previous = new RouterConfig();
|
||||
var current = new RouterConfig();
|
||||
|
||||
// Act
|
||||
var args = new ConfigChangedEventArgs(previous, current);
|
||||
|
||||
// Assert
|
||||
args.Previous.Should().BeSameAs(previous);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsCurrentConfig()
|
||||
{
|
||||
// Arrange
|
||||
var previous = new RouterConfig();
|
||||
var current = new RouterConfig();
|
||||
|
||||
// Act
|
||||
var args = new ConfigChangedEventArgs(previous, current);
|
||||
|
||||
// Assert
|
||||
args.Current.Should().BeSameAs(current);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsChangedAtToCurrentTime()
|
||||
{
|
||||
// Arrange
|
||||
var previous = new RouterConfig();
|
||||
var current = new RouterConfig();
|
||||
var beforeCreate = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
var args = new ConfigChangedEventArgs(previous, current);
|
||||
var afterCreate = DateTime.UtcNow;
|
||||
|
||||
// Assert
|
||||
args.ChangedAt.Should().BeOnOrAfter(beforeCreate);
|
||||
args.ChangedAt.Should().BeOnOrBefore(afterCreate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DifferentConfigs_BothAccessible()
|
||||
{
|
||||
// Arrange
|
||||
var previous = new RouterConfig
|
||||
{
|
||||
Routing = new RoutingOptions { LocalRegion = "us-west-1" }
|
||||
};
|
||||
var current = new RouterConfig
|
||||
{
|
||||
Routing = new RoutingOptions { LocalRegion = "us-east-1" }
|
||||
};
|
||||
|
||||
// Act
|
||||
var args = new ConfigChangedEventArgs(previous, current);
|
||||
|
||||
// Assert
|
||||
args.Previous.Routing.LocalRegion.Should().Be("us-west-1");
|
||||
args.Current.Routing.LocalRegion.Should().Be("us-east-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigChangedEventArgs_InheritsFromEventArgs()
|
||||
{
|
||||
// Arrange
|
||||
var previous = new RouterConfig();
|
||||
var current = new RouterConfig();
|
||||
|
||||
// Act
|
||||
var args = new ConfigChangedEventArgs(previous, current);
|
||||
|
||||
// Assert
|
||||
args.Should().BeAssignableTo<EventArgs>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="ConfigValidationResult"/>.
|
||||
/// </summary>
|
||||
public sealed class ConfigValidationResultTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Errors_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = new ConfigValidationResult();
|
||||
|
||||
// Assert
|
||||
result.Errors.Should().NotBeNull();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Warnings_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = new ConfigValidationResult();
|
||||
|
||||
// Assert
|
||||
result.Warnings.Should().NotBeNull();
|
||||
result.Warnings.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsValid Tests
|
||||
|
||||
[Fact]
|
||||
public void IsValid_NoErrors_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var result = new ConfigValidationResult();
|
||||
|
||||
// Act & Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_WithErrors_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var result = new ConfigValidationResult();
|
||||
result.Errors.Add("Some error");
|
||||
|
||||
// Act & Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_WithOnlyWarnings_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var result = new ConfigValidationResult();
|
||||
result.Warnings.Add("Some warning");
|
||||
|
||||
// Act & Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_WithErrorsAndWarnings_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var result = new ConfigValidationResult();
|
||||
result.Errors.Add("Some error");
|
||||
result.Warnings.Add("Some warning");
|
||||
|
||||
// Act & Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_MultipleErrors_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var result = new ConfigValidationResult();
|
||||
result.Errors.Add("Error 1");
|
||||
result.Errors.Add("Error 2");
|
||||
result.Errors.Add("Error 3");
|
||||
|
||||
// Act & Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Success Tests
|
||||
|
||||
[Fact]
|
||||
public void Success_ReturnsValidResult()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = ConfigValidationResult.Success;
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
result.Warnings.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Success_ReturnsNewInstance()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result1 = ConfigValidationResult.Success;
|
||||
var result2 = ConfigValidationResult.Success;
|
||||
|
||||
// Assert - Should be different instances to allow mutation without affecting shared state
|
||||
result1.Should().NotBeSameAs(result2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Errors Collection Tests
|
||||
|
||||
[Fact]
|
||||
public void Errors_CanBeModified()
|
||||
{
|
||||
// Arrange
|
||||
var result = new ConfigValidationResult();
|
||||
|
||||
// Act
|
||||
result.Errors.Add("Error 1");
|
||||
result.Errors.Add("Error 2");
|
||||
|
||||
// Assert
|
||||
result.Errors.Should().HaveCount(2);
|
||||
result.Errors.Should().Contain("Error 1");
|
||||
result.Errors.Should().Contain("Error 2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Errors_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = new ConfigValidationResult
|
||||
{
|
||||
Errors = ["Error 1", "Error 2"]
|
||||
};
|
||||
|
||||
// Assert
|
||||
result.Errors.Should().HaveCount(2);
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Warnings Collection Tests
|
||||
|
||||
[Fact]
|
||||
public void Warnings_CanBeModified()
|
||||
{
|
||||
// Arrange
|
||||
var result = new ConfigValidationResult();
|
||||
|
||||
// Act
|
||||
result.Warnings.Add("Warning 1");
|
||||
result.Warnings.Add("Warning 2");
|
||||
|
||||
// Assert
|
||||
result.Warnings.Should().HaveCount(2);
|
||||
result.Warnings.Should().Contain("Warning 1");
|
||||
result.Warnings.Should().Contain("Warning 2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Warnings_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = new ConfigValidationResult
|
||||
{
|
||||
Warnings = ["Warning 1", "Warning 2"]
|
||||
};
|
||||
|
||||
// Assert
|
||||
result.Warnings.Should().HaveCount(2);
|
||||
result.IsValid.Should().BeTrue(); // Warnings don't affect validity
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="RouterConfigOptions"/>.
|
||||
/// </summary>
|
||||
public sealed class RouterConfigOptionsTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ConfigPath_DefaultsToNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Assert
|
||||
options.ConfigPath.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_EnvironmentVariablePrefix_DefaultsToStellaOpsRouter()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Assert
|
||||
options.EnvironmentVariablePrefix.Should().Be("STELLAOPS_ROUTER_");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_EnableHotReload_DefaultsToTrue()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Assert
|
||||
options.EnableHotReload.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DebounceInterval_DefaultsTo500Milliseconds()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Assert
|
||||
options.DebounceInterval.Should().Be(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ThrowOnValidationError_DefaultsToFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Assert
|
||||
options.ThrowOnValidationError.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ConfigurationSection_DefaultsToRouter()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Assert
|
||||
options.ConfigurationSection.Should().Be("Router");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Assignment Tests
|
||||
|
||||
[Fact]
|
||||
public void ConfigPath_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Act
|
||||
options.ConfigPath = "/etc/stellaops/router.yaml";
|
||||
|
||||
// Assert
|
||||
options.ConfigPath.Should().Be("/etc/stellaops/router.yaml");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentVariablePrefix_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Act
|
||||
options.EnvironmentVariablePrefix = "CUSTOM_PREFIX_";
|
||||
|
||||
// Assert
|
||||
options.EnvironmentVariablePrefix.Should().Be("CUSTOM_PREFIX_");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnableHotReload_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Act
|
||||
options.EnableHotReload = false;
|
||||
|
||||
// Assert
|
||||
options.EnableHotReload.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DebounceInterval_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Act
|
||||
options.DebounceInterval = TimeSpan.FromSeconds(2);
|
||||
|
||||
// Assert
|
||||
options.DebounceInterval.Should().Be(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowOnValidationError_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Act
|
||||
options.ThrowOnValidationError = true;
|
||||
|
||||
// Assert
|
||||
options.ThrowOnValidationError.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigurationSection_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RouterConfigOptions();
|
||||
|
||||
// Act
|
||||
options.ConfigurationSection = "CustomSection";
|
||||
|
||||
// Assert
|
||||
options.ConfigurationSection.Should().Be("CustomSection");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="RouterConfigProvider"/> and configuration validation.
|
||||
/// </summary>
|
||||
public sealed class RouterConfigProviderTests : IDisposable
|
||||
{
|
||||
private readonly ILogger<RouterConfigProvider> _logger;
|
||||
private RouterConfigProvider? _provider;
|
||||
|
||||
public RouterConfigProviderTests()
|
||||
{
|
||||
_logger = NullLogger<RouterConfigProvider>.Instance;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_provider?.Dispose();
|
||||
}
|
||||
|
||||
private RouterConfigProvider CreateProvider(RouterConfigOptions? options = null)
|
||||
{
|
||||
var opts = Options.Create(options ?? new RouterConfigOptions { EnableHotReload = false });
|
||||
_provider = new RouterConfigProvider(opts, _logger);
|
||||
return _provider;
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitializesCurrentConfig()
|
||||
{
|
||||
// Arrange & Act
|
||||
var provider = CreateProvider();
|
||||
|
||||
// Assert
|
||||
provider.Current.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ExposesOptions()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RouterConfigOptions
|
||||
{
|
||||
ConfigPath = "/test/path.yaml",
|
||||
EnableHotReload = false
|
||||
};
|
||||
|
||||
// Act
|
||||
var provider = CreateProvider(options);
|
||||
|
||||
// Assert
|
||||
provider.Options.Should().NotBeNull();
|
||||
provider.Options.ConfigPath.Should().Be("/test/path.yaml");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithHotReloadDisabled_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RouterConfigOptions { EnableHotReload = false };
|
||||
|
||||
// Act
|
||||
var action = () => CreateProvider(options);
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate Tests - PayloadLimits
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidConfig_ReturnsIsValid()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZeroMaxRequestBytesPerCall_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.PayloadLimits = new PayloadLimits { MaxRequestBytesPerCall = 0 };
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("MaxRequestBytesPerCall"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NegativeMaxRequestBytesPerCall_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.PayloadLimits = new PayloadLimits { MaxRequestBytesPerCall = -1 };
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("MaxRequestBytesPerCall"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZeroMaxRequestBytesPerConnection_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.PayloadLimits = new PayloadLimits { MaxRequestBytesPerConnection = 0 };
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("MaxRequestBytesPerConnection"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZeroMaxAggregateInflightBytes_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.PayloadLimits = new PayloadLimits { MaxAggregateInflightBytes = 0 };
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("MaxAggregateInflightBytes"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MaxCallBytesLargerThanConnectionBytes_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.PayloadLimits = new PayloadLimits
|
||||
{
|
||||
MaxRequestBytesPerCall = 100 * 1024 * 1024,
|
||||
MaxRequestBytesPerConnection = 10 * 1024 * 1024,
|
||||
MaxAggregateInflightBytes = 1024 * 1024 * 1024
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue(); // Warnings don't fail validation
|
||||
result.Warnings.Should().Contain(w => w.Contains("MaxRequestBytesPerCall") && w.Contains("MaxRequestBytesPerConnection"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate Tests - RoutingOptions
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZeroDefaultTimeout_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.Routing.DefaultTimeout = TimeSpan.Zero;
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("DefaultTimeout"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NegativeDefaultTimeout_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.Routing.DefaultTimeout = TimeSpan.FromSeconds(-1);
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("DefaultTimeout"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate Tests - Services
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyServiceName_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.Services.Add(new ServiceConfig { ServiceName = "" });
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Service name cannot be empty"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhitespaceServiceName_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.Services.Add(new ServiceConfig { ServiceName = " " });
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Service name cannot be empty"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DuplicateServiceNames_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.Services.Add(new ServiceConfig { ServiceName = "my-service" });
|
||||
provider.Current.Services.Add(new ServiceConfig { ServiceName = "my-service" });
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Duplicate service name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DuplicateServiceNamesCaseInsensitive_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.Services.Add(new ServiceConfig { ServiceName = "MyService" });
|
||||
provider.Current.Services.Add(new ServiceConfig { ServiceName = "myservice" });
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Duplicate service name"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EndpointEmptyMethod_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.Services.Add(new ServiceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Endpoints = [new EndpointConfig { Method = "", Path = "/test" }]
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("endpoint method cannot be empty"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EndpointEmptyPath_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.Services.Add(new ServiceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Endpoints = [new EndpointConfig { Method = "GET", Path = "" }]
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("endpoint path cannot be empty"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EndpointNonPositiveTimeout_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.Services.Add(new ServiceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Endpoints = [new EndpointConfig { Method = "GET", Path = "/test", DefaultTimeout = TimeSpan.Zero }]
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue(); // Warnings don't fail validation
|
||||
result.Warnings.Should().Contain(w => w.Contains("non-positive timeout"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate Tests - StaticInstances
|
||||
|
||||
[Fact]
|
||||
public void Validate_StaticInstanceEmptyServiceName_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.StaticInstances.Add(new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("Static instance service name cannot be empty"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_StaticInstanceEmptyHost_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.StaticInstances.Add(new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "",
|
||||
Port = 8080
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("host cannot be empty"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(65536)]
|
||||
[InlineData(70000)]
|
||||
public void Validate_StaticInstanceInvalidPort_ReturnsError(int port)
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.StaticInstances.Add(new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = port
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.Contains("port must be between 1 and 65535"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(80)]
|
||||
[InlineData(443)]
|
||||
[InlineData(8080)]
|
||||
[InlineData(65535)]
|
||||
public void Validate_StaticInstanceValidPort_Succeeds(int port)
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.StaticInstances.Add(new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = port
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(-100)]
|
||||
public void Validate_StaticInstanceNonPositiveWeight_ReturnsWarning(int weight)
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.StaticInstances.Add(new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080,
|
||||
Weight = weight
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = provider.Validate();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue(); // Warnings don't fail validation
|
||||
result.Warnings.Should().Contain(w => w.Contains("weight should be positive"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ReloadAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ReloadAsync_ValidConfig_UpdatesCurrentConfig()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
|
||||
// Act
|
||||
await provider.ReloadAsync();
|
||||
|
||||
// Assert - Config should be reloaded (same content in this case since no file)
|
||||
provider.Current.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReloadAsync_InvalidConfig_ThrowsConfigurationException()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
provider.Current.PayloadLimits = new PayloadLimits { MaxRequestBytesPerCall = 0 };
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ConfigurationException>(() => provider.ReloadAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReloadAsync_Cancelled_ThrowsOperationCanceledException()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() => provider.ReloadAsync(cts.Token));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ConfigurationChanged Event Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ReloadAsync_RaisesConfigurationChangedEvent()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
ConfigChangedEventArgs? eventArgs = null;
|
||||
provider.ConfigurationChanged += (_, args) => eventArgs = args;
|
||||
|
||||
// Act
|
||||
await provider.ReloadAsync();
|
||||
|
||||
// Assert
|
||||
eventArgs.Should().NotBeNull();
|
||||
eventArgs!.Previous.Should().NotBeNull();
|
||||
eventArgs.Current.Should().NotBeNull();
|
||||
eventArgs.ChangedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dispose Tests
|
||||
|
||||
[Fact]
|
||||
public void Dispose_CanBeCalledMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
|
||||
// Act
|
||||
var action = () =>
|
||||
{
|
||||
provider.Dispose();
|
||||
provider.Dispose();
|
||||
provider.Dispose();
|
||||
};
|
||||
|
||||
// Assert
|
||||
action.Should().NotThrow();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="RouterConfig"/>.
|
||||
/// </summary>
|
||||
public sealed class RouterConfigTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PayloadLimits_DefaultsToNewInstance()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new RouterConfig();
|
||||
|
||||
// Assert
|
||||
config.PayloadLimits.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Routing_DefaultsToNewInstance()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new RouterConfig();
|
||||
|
||||
// Assert
|
||||
config.Routing.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Services_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new RouterConfig();
|
||||
|
||||
// Assert
|
||||
config.Services.Should().NotBeNull();
|
||||
config.Services.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_StaticInstances_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new RouterConfig();
|
||||
|
||||
// Assert
|
||||
config.StaticInstances.Should().NotBeNull();
|
||||
config.StaticInstances.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PayloadLimits Tests
|
||||
|
||||
[Fact]
|
||||
public void PayloadLimits_HasDefaultValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new RouterConfig();
|
||||
|
||||
// Assert
|
||||
config.PayloadLimits.MaxRequestBytesPerCall.Should().Be(10 * 1024 * 1024); // 10 MB
|
||||
config.PayloadLimits.MaxRequestBytesPerConnection.Should().Be(100 * 1024 * 1024); // 100 MB
|
||||
config.PayloadLimits.MaxAggregateInflightBytes.Should().Be(1024 * 1024 * 1024); // 1 GB
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PayloadLimits_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var config = new RouterConfig();
|
||||
|
||||
// Act
|
||||
config.PayloadLimits = new PayloadLimits
|
||||
{
|
||||
MaxRequestBytesPerCall = 5 * 1024 * 1024,
|
||||
MaxRequestBytesPerConnection = 50 * 1024 * 1024,
|
||||
MaxAggregateInflightBytes = 500 * 1024 * 1024
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.PayloadLimits.MaxRequestBytesPerCall.Should().Be(5 * 1024 * 1024);
|
||||
config.PayloadLimits.MaxRequestBytesPerConnection.Should().Be(50 * 1024 * 1024);
|
||||
config.PayloadLimits.MaxAggregateInflightBytes.Should().Be(500 * 1024 * 1024);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Routing Tests
|
||||
|
||||
[Fact]
|
||||
public void Routing_HasDefaultValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new RouterConfig();
|
||||
|
||||
// Assert
|
||||
config.Routing.LocalRegion.Should().Be("default");
|
||||
config.Routing.TieBreaker.Should().Be(TieBreakerStrategy.RoundRobin);
|
||||
config.Routing.PreferLocalRegion.Should().BeTrue();
|
||||
config.Routing.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Routing_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var config = new RouterConfig();
|
||||
|
||||
// Act
|
||||
config.Routing = new RoutingOptions
|
||||
{
|
||||
LocalRegion = "us-east-1",
|
||||
TieBreaker = TieBreakerStrategy.LeastLoaded,
|
||||
PreferLocalRegion = false,
|
||||
DefaultTimeout = TimeSpan.FromMinutes(2),
|
||||
NeighborRegions = ["us-west-1", "eu-west-1"]
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.Routing.LocalRegion.Should().Be("us-east-1");
|
||||
config.Routing.TieBreaker.Should().Be(TieBreakerStrategy.LeastLoaded);
|
||||
config.Routing.PreferLocalRegion.Should().BeFalse();
|
||||
config.Routing.DefaultTimeout.Should().Be(TimeSpan.FromMinutes(2));
|
||||
config.Routing.NeighborRegions.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Services Tests
|
||||
|
||||
[Fact]
|
||||
public void Services_CanAddServices()
|
||||
{
|
||||
// Arrange
|
||||
var config = new RouterConfig();
|
||||
|
||||
// Act
|
||||
config.Services.Add(new ServiceConfig { ServiceName = "service-a" });
|
||||
config.Services.Add(new ServiceConfig { ServiceName = "service-b" });
|
||||
|
||||
// Assert
|
||||
config.Services.Should().HaveCount(2);
|
||||
config.Services[0].ServiceName.Should().Be("service-a");
|
||||
config.Services[1].ServiceName.Should().Be("service-b");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Services_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new RouterConfig
|
||||
{
|
||||
Services =
|
||||
[
|
||||
new ServiceConfig { ServiceName = "auth" },
|
||||
new ServiceConfig { ServiceName = "users" },
|
||||
new ServiceConfig { ServiceName = "orders" }
|
||||
]
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.Services.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StaticInstances Tests
|
||||
|
||||
[Fact]
|
||||
public void StaticInstances_CanAddInstances()
|
||||
{
|
||||
// Arrange
|
||||
var config = new RouterConfig();
|
||||
|
||||
// Act
|
||||
config.StaticInstances.Add(new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "legacy-service",
|
||||
Version = "1.0",
|
||||
Host = "legacy.internal",
|
||||
Port = 9000
|
||||
});
|
||||
|
||||
// Assert
|
||||
config.StaticInstances.Should().HaveCount(1);
|
||||
config.StaticInstances[0].ServiceName.Should().Be("legacy-service");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticInstances_CanBeInitialized()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new RouterConfig
|
||||
{
|
||||
StaticInstances =
|
||||
[
|
||||
new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "db-proxy",
|
||||
Version = "2.0",
|
||||
Host = "db-proxy-1.internal",
|
||||
Port = 5432
|
||||
},
|
||||
new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "db-proxy",
|
||||
Version = "2.0",
|
||||
Host = "db-proxy-2.internal",
|
||||
Port = 5432
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.StaticInstances.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complete Configuration Tests
|
||||
|
||||
[Fact]
|
||||
public void CompleteConfiguration_Works()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new RouterConfig
|
||||
{
|
||||
PayloadLimits = new PayloadLimits
|
||||
{
|
||||
MaxRequestBytesPerCall = 1024 * 1024,
|
||||
MaxRequestBytesPerConnection = 10 * 1024 * 1024,
|
||||
MaxAggregateInflightBytes = 100 * 1024 * 1024
|
||||
},
|
||||
Routing = new RoutingOptions
|
||||
{
|
||||
LocalRegion = "us-east-1",
|
||||
NeighborRegions = ["us-west-1"],
|
||||
TieBreaker = TieBreakerStrategy.ConsistentHash,
|
||||
PreferLocalRegion = true,
|
||||
DefaultTimeout = TimeSpan.FromSeconds(60)
|
||||
},
|
||||
Services =
|
||||
[
|
||||
new ServiceConfig
|
||||
{
|
||||
ServiceName = "api-gateway",
|
||||
DefaultVersion = "1.0.0",
|
||||
Endpoints =
|
||||
[
|
||||
new EndpointConfig { Method = "GET", Path = "/health" }
|
||||
]
|
||||
}
|
||||
],
|
||||
StaticInstances =
|
||||
[
|
||||
new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "api-gateway",
|
||||
Version = "1.0.0",
|
||||
Host = "api-1.internal",
|
||||
Port = 8080,
|
||||
Weight = 100
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.PayloadLimits.MaxRequestBytesPerCall.Should().Be(1024 * 1024);
|
||||
config.Routing.LocalRegion.Should().Be("us-east-1");
|
||||
config.Services.Should().HaveCount(1);
|
||||
config.StaticInstances.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="RoutingOptions"/> and <see cref="TieBreakerStrategy"/>.
|
||||
/// </summary>
|
||||
public sealed class RoutingOptionsTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_LocalRegion_DefaultsToDefault()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RoutingOptions();
|
||||
|
||||
// Assert
|
||||
options.LocalRegion.Should().Be("default");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NeighborRegions_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RoutingOptions();
|
||||
|
||||
// Assert
|
||||
options.NeighborRegions.Should().NotBeNull();
|
||||
options.NeighborRegions.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_TieBreaker_DefaultsToRoundRobin()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RoutingOptions();
|
||||
|
||||
// Assert
|
||||
options.TieBreaker.Should().Be(TieBreakerStrategy.RoundRobin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PreferLocalRegion_DefaultsToTrue()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RoutingOptions();
|
||||
|
||||
// Assert
|
||||
options.PreferLocalRegion.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultTimeout_DefaultsTo30Seconds()
|
||||
{
|
||||
// Arrange & Act
|
||||
var options = new RoutingOptions();
|
||||
|
||||
// Assert
|
||||
options.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Assignment Tests
|
||||
|
||||
[Fact]
|
||||
public void LocalRegion_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions();
|
||||
|
||||
// Act
|
||||
options.LocalRegion = "us-east-1";
|
||||
|
||||
// Assert
|
||||
options.LocalRegion.Should().Be("us-east-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NeighborRegions_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions();
|
||||
|
||||
// Act
|
||||
options.NeighborRegions = ["us-west-1", "eu-west-1"];
|
||||
|
||||
// Assert
|
||||
options.NeighborRegions.Should().HaveCount(2);
|
||||
options.NeighborRegions.Should().Contain("us-west-1");
|
||||
options.NeighborRegions.Should().Contain("eu-west-1");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TieBreakerStrategy.RoundRobin)]
|
||||
[InlineData(TieBreakerStrategy.Random)]
|
||||
[InlineData(TieBreakerStrategy.LeastLoaded)]
|
||||
[InlineData(TieBreakerStrategy.ConsistentHash)]
|
||||
public void TieBreaker_CanBeSetToAllStrategies(TieBreakerStrategy strategy)
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions();
|
||||
|
||||
// Act
|
||||
options.TieBreaker = strategy;
|
||||
|
||||
// Assert
|
||||
options.TieBreaker.Should().Be(strategy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreferLocalRegion_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions();
|
||||
|
||||
// Act
|
||||
options.PreferLocalRegion = false;
|
||||
|
||||
// Assert
|
||||
options.PreferLocalRegion.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultTimeout_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var options = new RoutingOptions();
|
||||
|
||||
// Act
|
||||
options.DefaultTimeout = TimeSpan.FromMinutes(5);
|
||||
|
||||
// Assert
|
||||
options.DefaultTimeout.Should().Be(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TieBreakerStrategy Enum Tests
|
||||
|
||||
[Fact]
|
||||
public void TieBreakerStrategy_HasFourValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
var values = Enum.GetValues<TieBreakerStrategy>();
|
||||
|
||||
// Assert
|
||||
values.Should().HaveCount(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TieBreakerStrategy_RoundRobin_HasValueZero()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
((int)TieBreakerStrategy.RoundRobin).Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TieBreakerStrategy_Random_HasValueOne()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
((int)TieBreakerStrategy.Random).Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TieBreakerStrategy_LeastLoaded_HasValueTwo()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
((int)TieBreakerStrategy.LeastLoaded).Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TieBreakerStrategy_ConsistentHash_HasValueThree()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
((int)TieBreakerStrategy.ConsistentHash).Should().Be(3);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="ServiceConfig"/> and <see cref="EndpointConfig"/>.
|
||||
/// </summary>
|
||||
public sealed class ServiceConfigTests
|
||||
{
|
||||
#region ServiceConfig Default Values Tests
|
||||
|
||||
[Fact]
|
||||
public void ServiceConfig_DefaultVersion_DefaultsToNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new ServiceConfig { ServiceName = "test" };
|
||||
|
||||
// Assert
|
||||
config.DefaultVersion.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServiceConfig_DefaultTransport_DefaultsToTcp()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new ServiceConfig { ServiceName = "test" };
|
||||
|
||||
// Assert
|
||||
config.DefaultTransport.Should().Be(TransportType.Tcp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServiceConfig_Endpoints_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new ServiceConfig { ServiceName = "test" };
|
||||
|
||||
// Assert
|
||||
config.Endpoints.Should().NotBeNull();
|
||||
config.Endpoints.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ServiceConfig Property Assignment Tests
|
||||
|
||||
[Fact]
|
||||
public void ServiceConfig_ServiceName_CanBeSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new ServiceConfig { ServiceName = "my-service" };
|
||||
|
||||
// Assert
|
||||
config.ServiceName.Should().Be("my-service");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServiceConfig_DefaultVersion_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ServiceConfig { ServiceName = "test" };
|
||||
|
||||
// Act
|
||||
config.DefaultVersion = "1.0.0";
|
||||
|
||||
// Assert
|
||||
config.DefaultVersion.Should().Be("1.0.0");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransportType.Tcp)]
|
||||
[InlineData(TransportType.Certificate)]
|
||||
[InlineData(TransportType.Udp)]
|
||||
[InlineData(TransportType.InMemory)]
|
||||
[InlineData(TransportType.RabbitMq)]
|
||||
public void ServiceConfig_DefaultTransport_CanBeSetToAllTypes(TransportType transport)
|
||||
{
|
||||
// Arrange
|
||||
var config = new ServiceConfig { ServiceName = "test" };
|
||||
|
||||
// Act
|
||||
config.DefaultTransport = transport;
|
||||
|
||||
// Assert
|
||||
config.DefaultTransport.Should().Be(transport);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServiceConfig_Endpoints_CanAddEndpoints()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ServiceConfig { ServiceName = "test" };
|
||||
|
||||
// Act
|
||||
config.Endpoints.Add(new EndpointConfig { Method = "GET", Path = "/api/health" });
|
||||
config.Endpoints.Add(new EndpointConfig { Method = "POST", Path = "/api/data" });
|
||||
|
||||
// Assert
|
||||
config.Endpoints.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EndpointConfig Default Values Tests
|
||||
|
||||
[Fact]
|
||||
public void EndpointConfig_DefaultTimeout_DefaultsToNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
var endpoint = new EndpointConfig { Method = "GET", Path = "/" };
|
||||
|
||||
// Assert
|
||||
endpoint.DefaultTimeout.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointConfig_SupportsStreaming_DefaultsToFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
var endpoint = new EndpointConfig { Method = "GET", Path = "/" };
|
||||
|
||||
// Assert
|
||||
endpoint.SupportsStreaming.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointConfig_RequiringClaims_DefaultsToEmptyList()
|
||||
{
|
||||
// Arrange & Act
|
||||
var endpoint = new EndpointConfig { Method = "GET", Path = "/" };
|
||||
|
||||
// Assert
|
||||
endpoint.RequiringClaims.Should().NotBeNull();
|
||||
endpoint.RequiringClaims.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EndpointConfig Property Assignment Tests
|
||||
|
||||
[Fact]
|
||||
public void EndpointConfig_Method_CanBeSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var endpoint = new EndpointConfig { Method = "DELETE", Path = "/" };
|
||||
|
||||
// Assert
|
||||
endpoint.Method.Should().Be("DELETE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointConfig_Path_CanBeSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var endpoint = new EndpointConfig { Method = "GET", Path = "/api/users/{id}" };
|
||||
|
||||
// Assert
|
||||
endpoint.Path.Should().Be("/api/users/{id}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointConfig_DefaultTimeout_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = new EndpointConfig { Method = "GET", Path = "/" };
|
||||
|
||||
// Act
|
||||
endpoint.DefaultTimeout = TimeSpan.FromSeconds(60);
|
||||
|
||||
// Assert
|
||||
endpoint.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(60));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointConfig_SupportsStreaming_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = new EndpointConfig { Method = "GET", Path = "/" };
|
||||
|
||||
// Act
|
||||
endpoint.SupportsStreaming = true;
|
||||
|
||||
// Assert
|
||||
endpoint.SupportsStreaming.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointConfig_RequiringClaims_CanAddClaims()
|
||||
{
|
||||
// Arrange
|
||||
var endpoint = new EndpointConfig { Method = "GET", Path = "/" };
|
||||
|
||||
// Act
|
||||
endpoint.RequiringClaims.Add(new ClaimRequirement { Type = "role", Value = "admin" });
|
||||
endpoint.RequiringClaims.Add(new ClaimRequirement { Type = "permission", Value = "read" });
|
||||
|
||||
// Assert
|
||||
endpoint.RequiringClaims.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Configuration Tests
|
||||
|
||||
[Fact]
|
||||
public void ServiceConfig_CompleteConfiguration_Works()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new ServiceConfig
|
||||
{
|
||||
ServiceName = "user-service",
|
||||
DefaultVersion = "2.0.0",
|
||||
DefaultTransport = TransportType.Certificate,
|
||||
Endpoints =
|
||||
[
|
||||
new EndpointConfig
|
||||
{
|
||||
Method = "GET",
|
||||
Path = "/api/users/{id}",
|
||||
DefaultTimeout = TimeSpan.FromSeconds(10),
|
||||
SupportsStreaming = false,
|
||||
RequiringClaims = [new ClaimRequirement { Type = "role", Value = "user" }]
|
||||
},
|
||||
new EndpointConfig
|
||||
{
|
||||
Method = "POST",
|
||||
Path = "/api/users",
|
||||
DefaultTimeout = TimeSpan.FromSeconds(30),
|
||||
SupportsStreaming = false,
|
||||
RequiringClaims = [new ClaimRequirement { Type = "role", Value = "admin" }]
|
||||
},
|
||||
new EndpointConfig
|
||||
{
|
||||
Method = "GET",
|
||||
Path = "/api/users/stream",
|
||||
DefaultTimeout = TimeSpan.FromMinutes(5),
|
||||
SupportsStreaming = true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.ServiceName.Should().Be("user-service");
|
||||
config.DefaultVersion.Should().Be("2.0.0");
|
||||
config.DefaultTransport.Should().Be(TransportType.Certificate);
|
||||
config.Endpoints.Should().HaveCount(3);
|
||||
config.Endpoints[0].RequiringClaims.Should().HaveCount(1);
|
||||
config.Endpoints[2].SupportsStreaming.Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
using StellaOps.Router.Common.Enums;
|
||||
|
||||
namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="StaticInstanceConfig"/>.
|
||||
/// </summary>
|
||||
public sealed class StaticInstanceConfigTests
|
||||
{
|
||||
#region Default Values Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Region_DefaultsToDefault()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.Region.Should().Be("default");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Transport_DefaultsToTcp()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.Transport.Should().Be(TransportType.Tcp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Weight_DefaultsTo100()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.Weight.Should().Be(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Metadata_DefaultsToEmptyDictionary()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.Metadata.Should().NotBeNull();
|
||||
config.Metadata.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Required Properties Tests
|
||||
|
||||
[Fact]
|
||||
public void ServiceName_IsRequired()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "required-service",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.ServiceName.Should().Be("required-service");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Version_IsRequired()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "2.3.4",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.Version.Should().Be("2.3.4");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Host_IsRequired()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "192.168.1.100",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.Host.Should().Be("192.168.1.100");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Port_IsRequired()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 443
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.Port.Should().Be(443);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Assignment Tests
|
||||
|
||||
[Fact]
|
||||
public void Region_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Act
|
||||
config.Region = "us-west-2";
|
||||
|
||||
// Assert
|
||||
config.Region.Should().Be("us-west-2");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TransportType.Tcp)]
|
||||
[InlineData(TransportType.Certificate)]
|
||||
[InlineData(TransportType.Udp)]
|
||||
[InlineData(TransportType.InMemory)]
|
||||
[InlineData(TransportType.RabbitMq)]
|
||||
public void Transport_CanBeSetToAllTypes(TransportType transport)
|
||||
{
|
||||
// Arrange
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Act
|
||||
config.Transport = transport;
|
||||
|
||||
// Assert
|
||||
config.Transport.Should().Be(transport);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(50)]
|
||||
[InlineData(100)]
|
||||
[InlineData(200)]
|
||||
[InlineData(1000)]
|
||||
public void Weight_CanBeSet(int weight)
|
||||
{
|
||||
// Arrange
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Act
|
||||
config.Weight = weight;
|
||||
|
||||
// Assert
|
||||
config.Weight.Should().Be(weight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Metadata_CanAddEntries()
|
||||
{
|
||||
// Arrange
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0",
|
||||
Host = "localhost",
|
||||
Port = 8080
|
||||
};
|
||||
|
||||
// Act
|
||||
config.Metadata["environment"] = "production";
|
||||
config.Metadata["cluster"] = "primary";
|
||||
|
||||
// Assert
|
||||
config.Metadata.Should().HaveCount(2);
|
||||
config.Metadata["environment"].Should().Be("production");
|
||||
config.Metadata["cluster"].Should().Be("primary");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Configuration Tests
|
||||
|
||||
[Fact]
|
||||
public void CompleteConfiguration_Works()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "user-service",
|
||||
Version = "3.2.1",
|
||||
Region = "eu-central-1",
|
||||
Host = "user-svc.internal.example.com",
|
||||
Port = 8443,
|
||||
Transport = TransportType.Certificate,
|
||||
Weight = 150,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["datacenter"] = "dc1",
|
||||
["rack"] = "rack-42",
|
||||
["shard"] = "primary"
|
||||
}
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.ServiceName.Should().Be("user-service");
|
||||
config.Version.Should().Be("3.2.1");
|
||||
config.Region.Should().Be("eu-central-1");
|
||||
config.Host.Should().Be("user-svc.internal.example.com");
|
||||
config.Port.Should().Be(8443);
|
||||
config.Transport.Should().Be(TransportType.Certificate);
|
||||
config.Weight.Should().Be(150);
|
||||
config.Metadata.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleInstances_CanHaveDifferentWeights()
|
||||
{
|
||||
// Arrange & Act
|
||||
var primary = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "api",
|
||||
Version = "1.0",
|
||||
Host = "primary.example.com",
|
||||
Port = 8080,
|
||||
Weight = 200
|
||||
};
|
||||
|
||||
var secondary = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "api",
|
||||
Version = "1.0",
|
||||
Host = "secondary.example.com",
|
||||
Port = 8080,
|
||||
Weight = 100
|
||||
};
|
||||
|
||||
var tertiary = new StaticInstanceConfig
|
||||
{
|
||||
ServiceName = "api",
|
||||
Version = "1.0",
|
||||
Host = "tertiary.example.com",
|
||||
Port = 8080,
|
||||
Weight = 50
|
||||
};
|
||||
|
||||
// Assert
|
||||
primary.Weight.Should().BeGreaterThan(secondary.Weight);
|
||||
secondary.Weight.Should().BeGreaterThan(tertiary.Weight);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<!-- Suppress CA2255 from OpenSSL auto-init shim included via Directory.Build.props -->
|
||||
<NoWarn>$(NoWarn);CA2255</NoWarn>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>StellaOps.Router.Config.Tests</RootNamespace>
|
||||
<!-- Disable Concelier test infrastructure (Mongo2Go, etc.) since not needed for Router tests -->
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Test SDK packages come from Directory.Build.props -->
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Router.Config\StellaOps.Router.Config.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Router.Testing\StellaOps.Router.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user