using FluentAssertions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using StellaOps.Microservice; using StellaOps.Router.Common.Models; using Xunit; namespace StellaOps.Microservice.Tests; /// /// Tests for EndpointOverrideMerger - verifies merge logic and precedence. /// public class EndpointOverrideMergerTests { private readonly EndpointOverrideMerger _merger; private readonly Mock> _loggerMock; public EndpointOverrideMergerTests() { _loggerMock = new Mock>(); _merger = new EndpointOverrideMerger(_loggerMock.Object); } [Fact] public void Merge_WithNullYamlConfig_ReturnsCodeEndpointsUnchanged() { var codeEndpoints = new List { CreateEndpoint("GET", "/api/test", TimeSpan.FromSeconds(30)) }; var result = _merger.Merge(codeEndpoints, null); result.Should().BeEquivalentTo(codeEndpoints); } [Fact] public void Merge_WithEmptyYamlConfig_ReturnsCodeEndpointsUnchanged() { var codeEndpoints = new List { CreateEndpoint("GET", "/api/test", TimeSpan.FromSeconds(30)) }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [] }; var result = _merger.Merge(codeEndpoints, yamlConfig); result.Should().BeEquivalentTo(codeEndpoints); } [Fact] public void Merge_OverridesTimeout_WhenYamlSpecifiesTimeout() { var codeEndpoints = new List { CreateEndpoint("POST", "/api/generate", TimeSpan.FromSeconds(30)) }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [ new EndpointOverrideConfig { Method = "POST", Path = "/api/generate", DefaultTimeout = "5m" } ] }; var result = _merger.Merge(codeEndpoints, yamlConfig); result.Should().HaveCount(1); result[0].DefaultTimeout.Should().Be(TimeSpan.FromMinutes(5)); } [Fact] public void Merge_OverridesStreaming_WhenYamlSpecifiesStreaming() { var codeEndpoints = new List { CreateEndpoint("GET", "/api/data", TimeSpan.FromSeconds(30), supportsStreaming: false) }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [ new EndpointOverrideConfig { Method = "GET", Path = "/api/data", SupportsStreaming = true } ] }; var result = _merger.Merge(codeEndpoints, yamlConfig); result.Should().HaveCount(1); result[0].SupportsStreaming.Should().BeTrue(); } [Fact] public void Merge_OverridesClaims_WhenYamlSpecifiesClaims() { var codeEndpoints = new List { CreateEndpoint("DELETE", "/api/users/{id}", TimeSpan.FromSeconds(30)) }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [ new EndpointOverrideConfig { Method = "DELETE", Path = "/api/users/{id}", RequiringClaims = [ new ClaimRequirementConfig { Type = "role", Value = "admin" } ] } ] }; var result = _merger.Merge(codeEndpoints, yamlConfig); result.Should().HaveCount(1); result[0].RequiringClaims.Should().HaveCount(1); result[0].RequiringClaims![0].Type.Should().Be("role"); result[0].RequiringClaims[0].Value.Should().Be("admin"); } [Fact] public void Merge_PreservesCodeDefaults_WhenYamlDoesNotOverride() { var originalTimeout = TimeSpan.FromSeconds(45); var codeEndpoints = new List { CreateEndpoint("GET", "/api/test", originalTimeout, supportsStreaming: true) }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [ new EndpointOverrideConfig { Method = "GET", Path = "/api/test" // No overrides specified } ] }; var result = _merger.Merge(codeEndpoints, yamlConfig); result.Should().HaveCount(1); result[0].DefaultTimeout.Should().Be(originalTimeout); result[0].SupportsStreaming.Should().BeTrue(); } [Fact] public void Merge_MatchesCaseInsensitively() { var codeEndpoints = new List { CreateEndpoint("GET", "/api/Test", TimeSpan.FromSeconds(30)) }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [ new EndpointOverrideConfig { Method = "get", // lowercase Path = "/API/TEST", // uppercase DefaultTimeout = "1m" } ] }; var result = _merger.Merge(codeEndpoints, yamlConfig); result.Should().HaveCount(1); result[0].DefaultTimeout.Should().Be(TimeSpan.FromMinutes(1)); } [Fact] public void Merge_LeavesUnmatchedEndpointsUnchanged() { var codeEndpoints = new List { CreateEndpoint("GET", "/api/one", TimeSpan.FromSeconds(10)), CreateEndpoint("POST", "/api/two", TimeSpan.FromSeconds(20)), CreateEndpoint("PUT", "/api/three", TimeSpan.FromSeconds(30)) }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [ new EndpointOverrideConfig { Method = "POST", Path = "/api/two", DefaultTimeout = "5m" } ] }; var result = _merger.Merge(codeEndpoints, yamlConfig); result.Should().HaveCount(3); result[0].DefaultTimeout.Should().Be(TimeSpan.FromSeconds(10)); // unchanged result[1].DefaultTimeout.Should().Be(TimeSpan.FromMinutes(5)); // overridden result[2].DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30)); // unchanged } [Fact] public void Merge_LogsWarning_WhenYamlOverrideDoesNotMatchAnyEndpoint() { var codeEndpoints = new List { CreateEndpoint("GET", "/api/existing", TimeSpan.FromSeconds(30)) }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [ new EndpointOverrideConfig { Method = "POST", Path = "/api/nonexistent", DefaultTimeout = "5m" } ] }; _merger.Merge(codeEndpoints, yamlConfig); _loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), It.Is((v, t) => v.ToString()!.Contains("does not match any code endpoint")), It.IsAny(), It.IsAny>()), Times.Once); } [Fact] public void Merge_AppliesMultipleOverrides() { var codeEndpoints = new List { CreateEndpoint("GET", "/api/one", TimeSpan.FromSeconds(10)), CreateEndpoint("POST", "/api/two", TimeSpan.FromSeconds(20)) }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [ new EndpointOverrideConfig { Method = "GET", Path = "/api/one", DefaultTimeout = "1m" }, new EndpointOverrideConfig { Method = "POST", Path = "/api/two", DefaultTimeout = "2m" } ] }; var result = _merger.Merge(codeEndpoints, yamlConfig); result.Should().HaveCount(2); result[0].DefaultTimeout.Should().Be(TimeSpan.FromMinutes(1)); result[1].DefaultTimeout.Should().Be(TimeSpan.FromMinutes(2)); } [Fact] public void Merge_PreservesOriginalEndpointProperties() { var codeEndpoints = new List { new() { ServiceName = "test-service", Version = "2.0.0", Method = "GET", Path = "/api/test", DefaultTimeout = TimeSpan.FromSeconds(30), SupportsStreaming = false, HandlerType = typeof(object) } }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [ new EndpointOverrideConfig { Method = "GET", Path = "/api/test", DefaultTimeout = "1m" } ] }; var result = _merger.Merge(codeEndpoints, yamlConfig); result.Should().HaveCount(1); result[0].ServiceName.Should().Be("test-service"); result[0].Version.Should().Be("2.0.0"); result[0].Method.Should().Be("GET"); result[0].Path.Should().Be("/api/test"); result[0].DefaultTimeout.Should().Be(TimeSpan.FromMinutes(1)); result[0].HandlerType.Should().Be(typeof(object)); } [Fact] public void Merge_YamlOverridesCodeClaims_Completely() { var codeEndpoints = new List { new() { ServiceName = "test-service", Version = "1.0.0", Method = "GET", Path = "/api/test", DefaultTimeout = TimeSpan.FromSeconds(30), RequiringClaims = [ new ClaimRequirement { Type = "original", Value = "claim" } ] } }; var yamlConfig = new MicroserviceYamlConfig { Endpoints = [ new EndpointOverrideConfig { Method = "GET", Path = "/api/test", RequiringClaims = [ new ClaimRequirementConfig { Type = "new", Value = "claim1" }, new ClaimRequirementConfig { Type = "new", Value = "claim2" } ] } ] }; var result = _merger.Merge(codeEndpoints, yamlConfig); result[0].RequiringClaims.Should().HaveCount(2); result[0].RequiringClaims!.All(c => c.Type == "new").Should().BeTrue(); } private static EndpointDescriptor CreateEndpoint( string method, string path, TimeSpan timeout, bool supportsStreaming = false) { return new EndpointDescriptor { ServiceName = "test-service", Version = "1.0.0", Method = method, Path = path, DefaultTimeout = timeout, SupportsStreaming = supportsStreaming }; } }