Add unit tests for Router configuration and transport layers
- Implemented tests for RouterConfig, RoutingOptions, StaticInstanceConfig, and RouterConfigOptions to ensure default values are set correctly. - Added tests for RouterConfigProvider to validate configurations and ensure defaults are returned when no file is specified. - Created tests for ConfigValidationResult to check success and error scenarios. - Developed tests for ServiceCollectionExtensions to verify service registration for RouterConfig. - Introduced UdpTransportTests to validate serialization, connection, request-response, and error handling in UDP transport. - Added scripts for signing authority gaps and hashing DevPortal SDK snippets.
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for EndpointDiscoveryService - verifies integration of discovery + YAML loading + merging.
|
||||
/// </summary>
|
||||
public class EndpointDiscoveryServiceTests
|
||||
{
|
||||
private readonly Mock<IEndpointDiscoveryProvider> _discoveryProviderMock;
|
||||
private readonly Mock<IMicroserviceYamlLoader> _yamlLoaderMock;
|
||||
private readonly Mock<IEndpointOverrideMerger> _mergerMock;
|
||||
private readonly ILogger<EndpointDiscoveryService> _logger;
|
||||
private readonly EndpointDiscoveryService _service;
|
||||
|
||||
public EndpointDiscoveryServiceTests()
|
||||
{
|
||||
_discoveryProviderMock = new Mock<IEndpointDiscoveryProvider>();
|
||||
_yamlLoaderMock = new Mock<IMicroserviceYamlLoader>();
|
||||
_mergerMock = new Mock<IEndpointOverrideMerger>();
|
||||
_logger = NullLogger<EndpointDiscoveryService>.Instance;
|
||||
|
||||
_service = new EndpointDiscoveryService(
|
||||
_discoveryProviderMock.Object,
|
||||
_yamlLoaderMock.Object,
|
||||
_mergerMock.Object,
|
||||
_logger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_CallsDiscoveryProvider()
|
||||
{
|
||||
var codeEndpoints = new List<EndpointDescriptor>();
|
||||
_discoveryProviderMock
|
||||
.Setup(x => x.DiscoverEndpoints())
|
||||
.Returns(codeEndpoints);
|
||||
_mergerMock
|
||||
.Setup(x => x.Merge(It.IsAny<IReadOnlyList<EndpointDescriptor>>(), It.IsAny<MicroserviceYamlConfig?>()))
|
||||
.Returns(codeEndpoints);
|
||||
|
||||
_service.DiscoverEndpoints();
|
||||
|
||||
_discoveryProviderMock.Verify(x => x.DiscoverEndpoints(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_CallsYamlLoader()
|
||||
{
|
||||
var codeEndpoints = new List<EndpointDescriptor>();
|
||||
_discoveryProviderMock
|
||||
.Setup(x => x.DiscoverEndpoints())
|
||||
.Returns(codeEndpoints);
|
||||
_mergerMock
|
||||
.Setup(x => x.Merge(It.IsAny<IReadOnlyList<EndpointDescriptor>>(), It.IsAny<MicroserviceYamlConfig?>()))
|
||||
.Returns(codeEndpoints);
|
||||
|
||||
_service.DiscoverEndpoints();
|
||||
|
||||
_yamlLoaderMock.Verify(x => x.Load(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_PassesCodeEndpointsAndYamlConfigToMerger()
|
||||
{
|
||||
var codeEndpoints = new List<EndpointDescriptor>
|
||||
{
|
||||
CreateEndpoint("GET", "/api/test")
|
||||
};
|
||||
var yamlConfig = new MicroserviceYamlConfig
|
||||
{
|
||||
Endpoints =
|
||||
[
|
||||
new EndpointOverrideConfig { Method = "GET", Path = "/api/test" }
|
||||
]
|
||||
};
|
||||
|
||||
_discoveryProviderMock
|
||||
.Setup(x => x.DiscoverEndpoints())
|
||||
.Returns(codeEndpoints);
|
||||
_yamlLoaderMock
|
||||
.Setup(x => x.Load())
|
||||
.Returns(yamlConfig);
|
||||
_mergerMock
|
||||
.Setup(x => x.Merge(codeEndpoints, yamlConfig))
|
||||
.Returns(codeEndpoints);
|
||||
|
||||
_service.DiscoverEndpoints();
|
||||
|
||||
_mergerMock.Verify(x => x.Merge(codeEndpoints, yamlConfig), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_ReturnsMergedEndpoints()
|
||||
{
|
||||
var codeEndpoints = new List<EndpointDescriptor>
|
||||
{
|
||||
CreateEndpoint("GET", "/api/test", TimeSpan.FromSeconds(10))
|
||||
};
|
||||
var mergedEndpoints = new List<EndpointDescriptor>
|
||||
{
|
||||
CreateEndpoint("GET", "/api/test", TimeSpan.FromMinutes(5))
|
||||
};
|
||||
|
||||
_discoveryProviderMock
|
||||
.Setup(x => x.DiscoverEndpoints())
|
||||
.Returns(codeEndpoints);
|
||||
_mergerMock
|
||||
.Setup(x => x.Merge(It.IsAny<IReadOnlyList<EndpointDescriptor>>(), It.IsAny<MicroserviceYamlConfig?>()))
|
||||
.Returns(mergedEndpoints);
|
||||
|
||||
var result = _service.DiscoverEndpoints();
|
||||
|
||||
result.Should().BeSameAs(mergedEndpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_ContinuesWithNullYamlConfig_WhenLoaderReturnsNull()
|
||||
{
|
||||
var codeEndpoints = new List<EndpointDescriptor>
|
||||
{
|
||||
CreateEndpoint("GET", "/api/test")
|
||||
};
|
||||
|
||||
_discoveryProviderMock
|
||||
.Setup(x => x.DiscoverEndpoints())
|
||||
.Returns(codeEndpoints);
|
||||
_yamlLoaderMock
|
||||
.Setup(x => x.Load())
|
||||
.Returns((MicroserviceYamlConfig?)null);
|
||||
_mergerMock
|
||||
.Setup(x => x.Merge(codeEndpoints, null))
|
||||
.Returns(codeEndpoints);
|
||||
|
||||
var result = _service.DiscoverEndpoints();
|
||||
|
||||
_mergerMock.Verify(x => x.Merge(codeEndpoints, null), Times.Once);
|
||||
result.Should().BeSameAs(codeEndpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoverEndpoints_ContinuesWithNullYamlConfig_WhenLoaderThrows()
|
||||
{
|
||||
var codeEndpoints = new List<EndpointDescriptor>
|
||||
{
|
||||
CreateEndpoint("GET", "/api/test")
|
||||
};
|
||||
|
||||
_discoveryProviderMock
|
||||
.Setup(x => x.DiscoverEndpoints())
|
||||
.Returns(codeEndpoints);
|
||||
_yamlLoaderMock
|
||||
.Setup(x => x.Load())
|
||||
.Throws(new Exception("YAML parsing failed"));
|
||||
_mergerMock
|
||||
.Setup(x => x.Merge(codeEndpoints, null))
|
||||
.Returns(codeEndpoints);
|
||||
|
||||
var result = _service.DiscoverEndpoints();
|
||||
|
||||
// Should not throw, should continue with null config
|
||||
_mergerMock.Verify(x => x.Merge(codeEndpoints, null), Times.Once);
|
||||
result.Should().BeSameAs(codeEndpoints);
|
||||
}
|
||||
|
||||
private static EndpointDescriptor CreateEndpoint(
|
||||
string method,
|
||||
string path,
|
||||
TimeSpan? timeout = null)
|
||||
{
|
||||
return new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Method = method,
|
||||
Path = path,
|
||||
DefaultTimeout = timeout ?? TimeSpan.FromSeconds(30)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for EndpointOverrideMerger - verifies merge logic and precedence.
|
||||
/// </summary>
|
||||
public class EndpointOverrideMergerTests
|
||||
{
|
||||
private readonly EndpointOverrideMerger _merger;
|
||||
private readonly Mock<ILogger<EndpointOverrideMerger>> _loggerMock;
|
||||
|
||||
public EndpointOverrideMergerTests()
|
||||
{
|
||||
_loggerMock = new Mock<ILogger<EndpointOverrideMerger>>();
|
||||
_merger = new EndpointOverrideMerger(_loggerMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_WithNullYamlConfig_ReturnsCodeEndpointsUnchanged()
|
||||
{
|
||||
var codeEndpoints = new List<EndpointDescriptor>
|
||||
{
|
||||
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<EndpointDescriptor>
|
||||
{
|
||||
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<EndpointDescriptor>
|
||||
{
|
||||
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<EndpointDescriptor>
|
||||
{
|
||||
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<EndpointDescriptor>
|
||||
{
|
||||
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<EndpointDescriptor>
|
||||
{
|
||||
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<EndpointDescriptor>
|
||||
{
|
||||
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<EndpointDescriptor>
|
||||
{
|
||||
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<EndpointDescriptor>
|
||||
{
|
||||
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<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("does not match any code endpoint")),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_AppliesMultipleOverrides()
|
||||
{
|
||||
var codeEndpoints = new List<EndpointDescriptor>
|
||||
{
|
||||
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<EndpointDescriptor>
|
||||
{
|
||||
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<EndpointDescriptor>
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
169
tests/StellaOps.Microservice.Tests/EndpointRegistryTests.cs
Normal file
169
tests/StellaOps.Microservice.Tests/EndpointRegistryTests.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Microservice;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Microservice.Tests;
|
||||
|
||||
public class EndpointRegistryTests
|
||||
{
|
||||
private static EndpointDescriptor CreateEndpoint(string method, string path, Type? handlerType = null)
|
||||
{
|
||||
return new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Method = method,
|
||||
Path = path,
|
||||
HandlerType = handlerType
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_ExactMatch_ReturnsEndpoint()
|
||||
{
|
||||
var registry = new EndpointRegistry();
|
||||
var endpoint = CreateEndpoint("GET", "/api/users");
|
||||
registry.Register(endpoint);
|
||||
|
||||
var result = registry.TryMatch("GET", "/api/users", out var match);
|
||||
|
||||
result.Should().BeTrue();
|
||||
match.Should().NotBeNull();
|
||||
match!.Endpoint.Should().Be(endpoint);
|
||||
match.PathParameters.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_MethodMismatch_ReturnsFalse()
|
||||
{
|
||||
var registry = new EndpointRegistry();
|
||||
registry.Register(CreateEndpoint("GET", "/api/users"));
|
||||
|
||||
var result = registry.TryMatch("POST", "/api/users", out var match);
|
||||
|
||||
result.Should().BeFalse();
|
||||
match.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_PathMismatch_ReturnsFalse()
|
||||
{
|
||||
var registry = new EndpointRegistry();
|
||||
registry.Register(CreateEndpoint("GET", "/api/users"));
|
||||
|
||||
var result = registry.TryMatch("GET", "/api/products", out var match);
|
||||
|
||||
result.Should().BeFalse();
|
||||
match.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_WithPathParameter_ExtractsParameter()
|
||||
{
|
||||
var registry = new EndpointRegistry();
|
||||
registry.Register(CreateEndpoint("GET", "/api/users/{id}"));
|
||||
|
||||
var result = registry.TryMatch("GET", "/api/users/123", out var match);
|
||||
|
||||
result.Should().BeTrue();
|
||||
match.Should().NotBeNull();
|
||||
match!.PathParameters.Should().ContainKey("id");
|
||||
match.PathParameters["id"].Should().Be("123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_MethodCaseInsensitive_ReturnsMatch()
|
||||
{
|
||||
var registry = new EndpointRegistry();
|
||||
registry.Register(CreateEndpoint("GET", "/api/users"));
|
||||
|
||||
var result = registry.TryMatch("get", "/api/users", out var match);
|
||||
|
||||
result.Should().BeTrue();
|
||||
match.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_PathCaseInsensitive_ReturnsMatch()
|
||||
{
|
||||
var registry = new EndpointRegistry();
|
||||
registry.Register(CreateEndpoint("GET", "/api/users"));
|
||||
|
||||
var result = registry.TryMatch("GET", "/API/USERS", out var match);
|
||||
|
||||
result.Should().BeTrue();
|
||||
match.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterAll_MultipeEndpoints_AllRegistered()
|
||||
{
|
||||
var registry = new EndpointRegistry();
|
||||
var endpoints = new[]
|
||||
{
|
||||
CreateEndpoint("GET", "/api/users"),
|
||||
CreateEndpoint("POST", "/api/users"),
|
||||
CreateEndpoint("GET", "/api/users/{id}")
|
||||
};
|
||||
|
||||
registry.RegisterAll(endpoints);
|
||||
|
||||
registry.GetAllEndpoints().Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllEndpoints_ReturnsAllRegistered()
|
||||
{
|
||||
var registry = new EndpointRegistry();
|
||||
var endpoint1 = CreateEndpoint("GET", "/api/users");
|
||||
var endpoint2 = CreateEndpoint("POST", "/api/users");
|
||||
registry.Register(endpoint1);
|
||||
registry.Register(endpoint2);
|
||||
|
||||
var all = registry.GetAllEndpoints();
|
||||
|
||||
all.Should().HaveCount(2);
|
||||
all.Should().Contain(endpoint1);
|
||||
all.Should().Contain(endpoint2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_FirstMatchWins_WhenMultiplePossible()
|
||||
{
|
||||
var registry = new EndpointRegistry();
|
||||
var endpoint1 = CreateEndpoint("GET", "/api/users/{id}");
|
||||
var endpoint2 = CreateEndpoint("GET", "/api/{resource}/{id}");
|
||||
registry.Register(endpoint1);
|
||||
registry.Register(endpoint2);
|
||||
|
||||
var result = registry.TryMatch("GET", "/api/users/123", out var match);
|
||||
|
||||
result.Should().BeTrue();
|
||||
match.Should().NotBeNull();
|
||||
// First registered endpoint should match
|
||||
match!.Endpoint.Should().Be(endpoint1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_EmptyRegistry_ReturnsFalse()
|
||||
{
|
||||
var registry = new EndpointRegistry();
|
||||
|
||||
var result = registry.TryMatch("GET", "/api/users", out var match);
|
||||
|
||||
result.Should().BeFalse();
|
||||
match.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CaseSensitive_RespectsSetting()
|
||||
{
|
||||
var registry = new EndpointRegistry(caseInsensitive: false);
|
||||
registry.Register(CreateEndpoint("GET", "/api/users"));
|
||||
|
||||
var result = registry.TryMatch("GET", "/API/USERS", out var match);
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Microservice;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Microservice.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for MicroserviceYamlConfig and EndpointOverrideConfig classes.
|
||||
/// </summary>
|
||||
public class MicroserviceYamlConfigTests
|
||||
{
|
||||
[Fact]
|
||||
public void MicroserviceYamlConfig_DefaultsToEmptyEndpoints()
|
||||
{
|
||||
var config = new MicroserviceYamlConfig();
|
||||
|
||||
config.Endpoints.Should().NotBeNull();
|
||||
config.Endpoints.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndpointOverrideConfig_DefaultsToEmptyStrings()
|
||||
{
|
||||
var config = new EndpointOverrideConfig();
|
||||
|
||||
config.Method.Should().Be(string.Empty);
|
||||
config.Path.Should().Be(string.Empty);
|
||||
config.DefaultTimeout.Should().BeNull();
|
||||
config.SupportsStreaming.Should().BeNull();
|
||||
config.RequiringClaims.Should().BeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("30s", 30)]
|
||||
[InlineData("60s", 60)]
|
||||
[InlineData("1s", 1)]
|
||||
[InlineData("120S", 120)] // Case insensitive
|
||||
public void GetDefaultTimeoutAsTimeSpan_ParsesSeconds(string input, int expectedSeconds)
|
||||
{
|
||||
var config = new EndpointOverrideConfig { DefaultTimeout = input };
|
||||
|
||||
var result = config.GetDefaultTimeoutAsTimeSpan();
|
||||
|
||||
result.Should().Be(TimeSpan.FromSeconds(expectedSeconds));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("5m", 5)]
|
||||
[InlineData("10m", 10)]
|
||||
[InlineData("1m", 1)]
|
||||
[InlineData("30M", 30)] // Case insensitive
|
||||
public void GetDefaultTimeoutAsTimeSpan_ParsesMinutes(string input, int expectedMinutes)
|
||||
{
|
||||
var config = new EndpointOverrideConfig { DefaultTimeout = input };
|
||||
|
||||
var result = config.GetDefaultTimeoutAsTimeSpan();
|
||||
|
||||
result.Should().Be(TimeSpan.FromMinutes(expectedMinutes));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("1h", 1)]
|
||||
[InlineData("2h", 2)]
|
||||
[InlineData("24h", 24)]
|
||||
[InlineData("1H", 1)] // Case insensitive
|
||||
public void GetDefaultTimeoutAsTimeSpan_ParsesHours(string input, int expectedHours)
|
||||
{
|
||||
var config = new EndpointOverrideConfig { DefaultTimeout = input };
|
||||
|
||||
var result = config.GetDefaultTimeoutAsTimeSpan();
|
||||
|
||||
result.Should().Be(TimeSpan.FromHours(expectedHours));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("00:00:30", 30)]
|
||||
[InlineData("00:05:00", 300)]
|
||||
[InlineData("01:00:00", 3600)]
|
||||
[InlineData("00:01:30", 90)]
|
||||
public void GetDefaultTimeoutAsTimeSpan_ParsesTimeSpanFormat(string input, int expectedSeconds)
|
||||
{
|
||||
var config = new EndpointOverrideConfig { DefaultTimeout = input };
|
||||
|
||||
var result = config.GetDefaultTimeoutAsTimeSpan();
|
||||
|
||||
result.Should().Be(TimeSpan.FromSeconds(expectedSeconds));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void GetDefaultTimeoutAsTimeSpan_ReturnsNullForEmptyValues(string? input)
|
||||
{
|
||||
var config = new EndpointOverrideConfig { DefaultTimeout = input };
|
||||
|
||||
var result = config.GetDefaultTimeoutAsTimeSpan();
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("invalid")]
|
||||
[InlineData("abc")]
|
||||
[InlineData("30x")]
|
||||
public void GetDefaultTimeoutAsTimeSpan_ReturnsNullForInvalidFormats(string input)
|
||||
{
|
||||
var config = new EndpointOverrideConfig { DefaultTimeout = input };
|
||||
|
||||
var result = config.GetDefaultTimeoutAsTimeSpan();
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClaimRequirementConfig_ToClaimRequirement_ConvertsCorrectly()
|
||||
{
|
||||
var config = new ClaimRequirementConfig
|
||||
{
|
||||
Type = "role",
|
||||
Value = "admin"
|
||||
};
|
||||
|
||||
var result = config.ToClaimRequirement();
|
||||
|
||||
result.Type.Should().Be("role");
|
||||
result.Value.Should().Be("admin");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClaimRequirementConfig_ToClaimRequirement_HandlesNullValue()
|
||||
{
|
||||
var config = new ClaimRequirementConfig
|
||||
{
|
||||
Type = "authenticated",
|
||||
Value = null
|
||||
};
|
||||
|
||||
var result = config.ToClaimRequirement();
|
||||
|
||||
result.Type.Should().Be("authenticated");
|
||||
result.Value.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Microservice;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Microservice.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for MicroserviceYamlLoader.
|
||||
/// </summary>
|
||||
public class MicroserviceYamlLoaderTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDirectory;
|
||||
private readonly ILogger<MicroserviceYamlLoader> _logger;
|
||||
|
||||
public MicroserviceYamlLoaderTests()
|
||||
{
|
||||
_tempDirectory = Path.Combine(Path.GetTempPath(), $"MicroserviceYamlLoaderTests_{Guid.NewGuid()}");
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
_logger = NullLogger<MicroserviceYamlLoader>.Instance;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_tempDirectory))
|
||||
{
|
||||
Directory.Delete(_tempDirectory, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ReturnsNull_WhenConfigFilePathIsNull()
|
||||
{
|
||||
var options = new StellaMicroserviceOptions
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0.0",
|
||||
Region = "us",
|
||||
ConfigFilePath = null
|
||||
};
|
||||
var loader = new MicroserviceYamlLoader(options, _logger);
|
||||
|
||||
var result = loader.Load();
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ReturnsNull_WhenConfigFilePathIsEmpty()
|
||||
{
|
||||
var options = new StellaMicroserviceOptions
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0.0",
|
||||
Region = "us",
|
||||
ConfigFilePath = ""
|
||||
};
|
||||
var loader = new MicroserviceYamlLoader(options, _logger);
|
||||
|
||||
var result = loader.Load();
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ReturnsNull_WhenFileDoesNotExist()
|
||||
{
|
||||
var options = new StellaMicroserviceOptions
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0.0",
|
||||
Region = "us",
|
||||
ConfigFilePath = Path.Combine(_tempDirectory, "nonexistent.yaml")
|
||||
};
|
||||
var loader = new MicroserviceYamlLoader(options, _logger);
|
||||
|
||||
var result = loader.Load();
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ParsesValidYaml()
|
||||
{
|
||||
var yamlContent = """
|
||||
endpoints:
|
||||
- method: GET
|
||||
path: /api/test
|
||||
defaultTimeout: 30s
|
||||
supportsStreaming: true
|
||||
""";
|
||||
var filePath = Path.Combine(_tempDirectory, "config.yaml");
|
||||
File.WriteAllText(filePath, yamlContent);
|
||||
var options = new StellaMicroserviceOptions
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0.0",
|
||||
Region = "us",
|
||||
ConfigFilePath = filePath
|
||||
};
|
||||
var loader = new MicroserviceYamlLoader(options, _logger);
|
||||
|
||||
var result = loader.Load();
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Endpoints.Should().HaveCount(1);
|
||||
result.Endpoints[0].Method.Should().Be("GET");
|
||||
result.Endpoints[0].Path.Should().Be("/api/test");
|
||||
result.Endpoints[0].DefaultTimeout.Should().Be("30s");
|
||||
result.Endpoints[0].SupportsStreaming.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ParsesMultipleEndpoints()
|
||||
{
|
||||
var yamlContent = """
|
||||
endpoints:
|
||||
- method: GET
|
||||
path: /api/one
|
||||
defaultTimeout: 10s
|
||||
- method: POST
|
||||
path: /api/two
|
||||
defaultTimeout: 5m
|
||||
- method: DELETE
|
||||
path: /api/three
|
||||
defaultTimeout: 1h
|
||||
""";
|
||||
var filePath = Path.Combine(_tempDirectory, "config.yaml");
|
||||
File.WriteAllText(filePath, yamlContent);
|
||||
var options = new StellaMicroserviceOptions
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0.0",
|
||||
Region = "us",
|
||||
ConfigFilePath = filePath
|
||||
};
|
||||
var loader = new MicroserviceYamlLoader(options, _logger);
|
||||
|
||||
var result = loader.Load();
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Endpoints.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ParsesClaimRequirements()
|
||||
{
|
||||
var yamlContent = """
|
||||
endpoints:
|
||||
- method: DELETE
|
||||
path: /api/admin
|
||||
requiringClaims:
|
||||
- type: role
|
||||
value: admin
|
||||
- type: permission
|
||||
value: delete
|
||||
""";
|
||||
var filePath = Path.Combine(_tempDirectory, "config.yaml");
|
||||
File.WriteAllText(filePath, yamlContent);
|
||||
var options = new StellaMicroserviceOptions
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0.0",
|
||||
Region = "us",
|
||||
ConfigFilePath = filePath
|
||||
};
|
||||
var loader = new MicroserviceYamlLoader(options, _logger);
|
||||
|
||||
var result = loader.Load();
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Endpoints.Should().HaveCount(1);
|
||||
result.Endpoints[0].RequiringClaims.Should().HaveCount(2);
|
||||
result.Endpoints[0].RequiringClaims![0].Type.Should().Be("role");
|
||||
result.Endpoints[0].RequiringClaims![0].Value.Should().Be("admin");
|
||||
result.Endpoints[0].RequiringClaims![1].Type.Should().Be("permission");
|
||||
result.Endpoints[0].RequiringClaims![1].Value.Should().Be("delete");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_HandlesEmptyEndpointsList()
|
||||
{
|
||||
var yamlContent = """
|
||||
endpoints: []
|
||||
""";
|
||||
var filePath = Path.Combine(_tempDirectory, "config.yaml");
|
||||
File.WriteAllText(filePath, yamlContent);
|
||||
var options = new StellaMicroserviceOptions
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0.0",
|
||||
Region = "us",
|
||||
ConfigFilePath = filePath
|
||||
};
|
||||
var loader = new MicroserviceYamlLoader(options, _logger);
|
||||
|
||||
var result = loader.Load();
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Endpoints.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_IgnoresUnknownProperties()
|
||||
{
|
||||
var yamlContent = """
|
||||
unknownProperty: value
|
||||
endpoints:
|
||||
- method: GET
|
||||
path: /api/test
|
||||
unknownField: ignored
|
||||
""";
|
||||
var filePath = Path.Combine(_tempDirectory, "config.yaml");
|
||||
File.WriteAllText(filePath, yamlContent);
|
||||
var options = new StellaMicroserviceOptions
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0.0",
|
||||
Region = "us",
|
||||
ConfigFilePath = filePath
|
||||
};
|
||||
var loader = new MicroserviceYamlLoader(options, _logger);
|
||||
|
||||
var result = loader.Load();
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Endpoints.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ThrowsOnInvalidYaml()
|
||||
{
|
||||
var yamlContent = """
|
||||
endpoints:
|
||||
- method: GET
|
||||
path /api/test # missing colon
|
||||
""";
|
||||
var filePath = Path.Combine(_tempDirectory, "config.yaml");
|
||||
File.WriteAllText(filePath, yamlContent);
|
||||
var options = new StellaMicroserviceOptions
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0.0",
|
||||
Region = "us",
|
||||
ConfigFilePath = filePath
|
||||
};
|
||||
var loader = new MicroserviceYamlLoader(options, _logger);
|
||||
|
||||
Action act = () => loader.Load();
|
||||
|
||||
act.Should().Throw<Exception>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ResolvesRelativePath()
|
||||
{
|
||||
var yamlContent = """
|
||||
endpoints:
|
||||
- method: GET
|
||||
path: /api/test
|
||||
""";
|
||||
var filePath = Path.Combine(_tempDirectory, "config.yaml");
|
||||
File.WriteAllText(filePath, yamlContent);
|
||||
|
||||
// Save current directory and change to temp directory
|
||||
var originalDirectory = Environment.CurrentDirectory;
|
||||
try
|
||||
{
|
||||
Environment.CurrentDirectory = _tempDirectory;
|
||||
var options = new StellaMicroserviceOptions
|
||||
{
|
||||
ServiceName = "test",
|
||||
Version = "1.0.0",
|
||||
Region = "us",
|
||||
ConfigFilePath = "config.yaml" // relative path
|
||||
};
|
||||
var loader = new MicroserviceYamlLoader(options, _logger);
|
||||
|
||||
var result = loader.Load();
|
||||
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.CurrentDirectory = originalDirectory;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,11 @@
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
192
tests/StellaOps.Microservice.Tests/TypedEndpointAdapterTests.cs
Normal file
192
tests/StellaOps.Microservice.Tests/TypedEndpointAdapterTests.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Microservice;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Microservice.Tests;
|
||||
|
||||
public class TypedEndpointAdapterTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public record TestRequest(string Name, int Value);
|
||||
public record TestResponse(string Message, bool Success);
|
||||
|
||||
public class TestTypedHandler : IStellaEndpoint<TestRequest, TestResponse>
|
||||
{
|
||||
public Task<TestResponse> HandleAsync(TestRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new TestResponse($"Hello, {request.Name}!", true));
|
||||
}
|
||||
}
|
||||
|
||||
public class TestNoRequestHandler : IStellaEndpoint<TestResponse>
|
||||
{
|
||||
public Task<TestResponse> HandleAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new TestResponse("No request needed", true));
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRawHandler : IRawStellaEndpoint
|
||||
{
|
||||
public Task<RawResponse> HandleAsync(RawRequestContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(RawResponse.Ok("Raw response"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Adapt_TypedWithRequest_DeserializesAndSerializes()
|
||||
{
|
||||
var handler = new TestTypedHandler();
|
||||
var adapter = TypedEndpointAdapter.Adapt<TestRequest, TestResponse>(handler);
|
||||
|
||||
var request = new TestRequest("World", 42);
|
||||
var requestBytes = JsonSerializer.SerializeToUtf8Bytes(request, JsonOptions);
|
||||
var context = new RawRequestContext
|
||||
{
|
||||
Method = "POST",
|
||||
Path = "/test",
|
||||
Body = new MemoryStream(requestBytes),
|
||||
Headers = HeaderCollection.Empty
|
||||
};
|
||||
|
||||
var response = await adapter(context, CancellationToken.None);
|
||||
|
||||
response.StatusCode.Should().Be(200);
|
||||
response.Headers["Content-Type"].Should().Contain("application/json");
|
||||
|
||||
var responseBody = await ReadResponseBody(response);
|
||||
var result = JsonSerializer.Deserialize<TestResponse>(responseBody, JsonOptions);
|
||||
result.Should().NotBeNull();
|
||||
result!.Message.Should().Be("Hello, World!");
|
||||
result.Success.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Adapt_TypedNoRequest_SerializesResponse()
|
||||
{
|
||||
var handler = new TestNoRequestHandler();
|
||||
var adapter = TypedEndpointAdapter.Adapt<TestResponse>(handler);
|
||||
|
||||
var context = new RawRequestContext
|
||||
{
|
||||
Method = "GET",
|
||||
Path = "/test",
|
||||
Body = Stream.Null,
|
||||
Headers = HeaderCollection.Empty
|
||||
};
|
||||
|
||||
var response = await adapter(context, CancellationToken.None);
|
||||
|
||||
response.StatusCode.Should().Be(200);
|
||||
|
||||
var responseBody = await ReadResponseBody(response);
|
||||
var result = JsonSerializer.Deserialize<TestResponse>(responseBody, JsonOptions);
|
||||
result.Should().NotBeNull();
|
||||
result!.Message.Should().Be("No request needed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Adapt_RawHandler_PassesThroughDirectly()
|
||||
{
|
||||
var handler = new TestRawHandler();
|
||||
var adapter = TypedEndpointAdapter.Adapt(handler);
|
||||
|
||||
var context = new RawRequestContext
|
||||
{
|
||||
Method = "GET",
|
||||
Path = "/test",
|
||||
Body = Stream.Null,
|
||||
Headers = HeaderCollection.Empty
|
||||
};
|
||||
|
||||
var response = await adapter(context, CancellationToken.None);
|
||||
|
||||
response.StatusCode.Should().Be(200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Adapt_InvalidJson_ReturnsBadRequest()
|
||||
{
|
||||
var handler = new TestTypedHandler();
|
||||
var adapter = TypedEndpointAdapter.Adapt<TestRequest, TestResponse>(handler);
|
||||
|
||||
var context = new RawRequestContext
|
||||
{
|
||||
Method = "POST",
|
||||
Path = "/test",
|
||||
Body = new MemoryStream(Encoding.UTF8.GetBytes("not valid json")),
|
||||
Headers = HeaderCollection.Empty
|
||||
};
|
||||
|
||||
var response = await adapter(context, CancellationToken.None);
|
||||
|
||||
response.StatusCode.Should().Be(400);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Adapt_EmptyBody_ReturnsBadRequest()
|
||||
{
|
||||
var handler = new TestTypedHandler();
|
||||
var adapter = TypedEndpointAdapter.Adapt<TestRequest, TestResponse>(handler);
|
||||
|
||||
var context = new RawRequestContext
|
||||
{
|
||||
Method = "POST",
|
||||
Path = "/test",
|
||||
Body = new MemoryStream([]),
|
||||
Headers = HeaderCollection.Empty
|
||||
};
|
||||
|
||||
var response = await adapter(context, CancellationToken.None);
|
||||
|
||||
response.StatusCode.Should().Be(400);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Adapt_WithCancellation_PropagatesCancellation()
|
||||
{
|
||||
var handler = new CancellableHandler();
|
||||
var adapter = TypedEndpointAdapter.Adapt<TestResponse>(handler);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var context = new RawRequestContext
|
||||
{
|
||||
Method = "GET",
|
||||
Path = "/test",
|
||||
Body = Stream.Null,
|
||||
Headers = HeaderCollection.Empty
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() =>
|
||||
adapter(context, cts.Token));
|
||||
}
|
||||
|
||||
private class CancellableHandler : IStellaEndpoint<TestResponse>
|
||||
{
|
||||
public Task<TestResponse> HandleAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(new TestResponse("OK", true));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadResponseBody(RawResponse response)
|
||||
{
|
||||
if (response.Body == Stream.Null)
|
||||
return string.Empty;
|
||||
|
||||
response.Body.Position = 0;
|
||||
using var reader = new StreamReader(response.Body);
|
||||
return await reader.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user