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
};
}
}