consolidate the tests locations

This commit is contained in:
StellaOps Bot
2025-12-26 01:48:24 +02:00
parent 17613acf57
commit 39359da171
2031 changed files with 2607 additions and 476 deletions

View File

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

View File

@@ -0,0 +1,128 @@
using System.Reflection;
using StellaOps.Microservice;
using Xunit;
namespace StellaOps.Microservice.Tests;
// Test endpoint classes
[StellaEndpoint("GET", "/api/test")]
public class TestGetEndpoint : IStellaEndpoint<string>
{
public Task<string> HandleAsync(CancellationToken cancellationToken) => Task.FromResult("OK");
}
[StellaEndpoint("POST", "/api/create", SupportsStreaming = true, TimeoutSeconds = 60)]
public class TestPostEndpoint : IStellaEndpoint<TestRequest, TestResponse>
{
public Task<TestResponse> HandleAsync(TestRequest request, CancellationToken cancellationToken)
=> Task.FromResult(new TestResponse());
}
public record TestRequest;
public record TestResponse;
public class EndpointDiscoveryTests
{
[Fact]
public void StellaEndpointAttribute_StoresMethodAndPath()
{
// Arrange & Act
var attr = new StellaEndpointAttribute("POST", "/api/test");
// Assert
Assert.Equal("POST", attr.Method);
Assert.Equal("/api/test", attr.Path);
}
[Fact]
public void StellaEndpointAttribute_NormalizesMethod()
{
// Arrange & Act
var attr = new StellaEndpointAttribute("get", "/api/test");
// Assert
Assert.Equal("GET", attr.Method);
}
[Fact]
public void StellaEndpointAttribute_DefaultTimeoutIs30Seconds()
{
// Arrange & Act
var attr = new StellaEndpointAttribute("GET", "/api/test");
// Assert
Assert.Equal(30, attr.TimeoutSeconds);
}
[Fact]
public void ReflectionDiscovery_FindsEndpointsInCurrentAssembly()
{
// Arrange
var options = new StellaMicroserviceOptions
{
ServiceName = "test-service",
Version = "1.0.0",
Region = "eu1"
};
var discovery = new ReflectionEndpointDiscoveryProvider(
options,
[Assembly.GetExecutingAssembly()]);
// Act
var endpoints = discovery.DiscoverEndpoints();
// Assert
Assert.True(endpoints.Count >= 2);
Assert.Contains(endpoints, e => e.Method == "GET" && e.Path == "/api/test");
Assert.Contains(endpoints, e => e.Method == "POST" && e.Path == "/api/create");
}
[Fact]
public void ReflectionDiscovery_SetsServiceNameAndVersion()
{
// Arrange
var options = new StellaMicroserviceOptions
{
ServiceName = "my-service",
Version = "2.0.0",
Region = "eu1"
};
var discovery = new ReflectionEndpointDiscoveryProvider(
options,
[Assembly.GetExecutingAssembly()]);
// Act
var endpoints = discovery.DiscoverEndpoints();
var endpoint = endpoints.First(e => e.Path == "/api/test");
// Assert
Assert.Equal("my-service", endpoint.ServiceName);
Assert.Equal("2.0.0", endpoint.Version);
}
[Fact]
public void ReflectionDiscovery_SetsStreamingAndTimeout()
{
// Arrange
var options = new StellaMicroserviceOptions
{
ServiceName = "test",
Version = "1.0.0",
Region = "eu1"
};
var discovery = new ReflectionEndpointDiscoveryProvider(
options,
[Assembly.GetExecutingAssembly()]);
// Act
var endpoints = discovery.DiscoverEndpoints();
var postEndpoint = endpoints.First(e => e.Path == "/api/create");
var getEndpoint = endpoints.First(e => e.Path == "/api/test");
// Assert
Assert.True(postEndpoint.SupportsStreaming);
Assert.Equal(TimeSpan.FromSeconds(60), postEndpoint.DefaultTimeout);
Assert.False(getEndpoint.SupportsStreaming);
Assert.Equal(TimeSpan.FromSeconds(30), getEndpoint.DefaultTimeout);
}
}

View File

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

View 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();
}
}

View File

@@ -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();
}
}

View File

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

View File

@@ -0,0 +1,164 @@
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Router.Common.Frames;
using StellaOps.Router.Common.Models;
using Xunit;
namespace StellaOps.Microservice.Tests;
public sealed class RequestDispatcherTests
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
[Fact]
public async Task DispatchAsync_WhenEndpointNotFound_Returns404()
{
var registry = new EndpointRegistry();
var services = new ServiceCollection();
using var provider = services.BuildServiceProvider();
var dispatcher = new RequestDispatcher(
registry,
provider,
CreateLogger<RequestDispatcher>());
var response = await dispatcher.DispatchAsync(
new RequestFrame
{
RequestId = "req-404",
Method = "GET",
Path = "/missing",
Payload = ReadOnlyMemory<byte>.Empty
},
CancellationToken.None);
response.StatusCode.Should().Be(404);
Encoding.UTF8.GetString(response.Payload.Span).Should().Be("Not Found");
}
[Fact]
public async Task DispatchAsync_WhenBodyEmpty_BindsFromPathAndQueryParameters()
{
var registry = new EndpointRegistry();
registry.Register(new EndpointDescriptor
{
ServiceName = "inventory",
Version = "1.0.0",
Method = "GET",
Path = "/items/{id}",
HandlerType = typeof(GetItemHandler)
});
var services = new ServiceCollection();
services.AddTransient<GetItemHandler>();
using var provider = services.BuildServiceProvider();
var dispatcher = new RequestDispatcher(
registry,
provider,
CreateLogger<RequestDispatcher>(),
jsonOptions: JsonOptions);
var response = await dispatcher.DispatchAsync(
new RequestFrame
{
RequestId = "req-params",
Method = "GET",
Path = "/items/123?filter=active",
Payload = ReadOnlyMemory<byte>.Empty
},
CancellationToken.None);
response.StatusCode.Should().Be(200);
response.Headers.Should().ContainKey("Content-Type");
var dto = JsonSerializer.Deserialize<GetItemResponse>(response.Payload.Span, JsonOptions);
dto.Should().NotBeNull();
dto!.Id.Should().Be(123);
dto.Filter.Should().Be("active");
}
[Fact]
public async Task DispatchAsync_WhenBodyPresent_PathAndQueryOverrideJsonProperties()
{
var registry = new EndpointRegistry();
registry.Register(new EndpointDescriptor
{
ServiceName = "inventory",
Version = "1.0.0",
Method = "POST",
Path = "/items/{id}",
HandlerType = typeof(GetItemHandler)
});
var services = new ServiceCollection();
services.AddTransient<GetItemHandler>();
using var provider = services.BuildServiceProvider();
var dispatcher = new RequestDispatcher(
registry,
provider,
CreateLogger<RequestDispatcher>(),
jsonOptions: JsonOptions);
var body = JsonSerializer.SerializeToUtf8Bytes(
new GetItemRequest { Id = 999, Filter = "fromBody" },
JsonOptions);
var response = await dispatcher.DispatchAsync(
new RequestFrame
{
RequestId = "req-body",
Method = "POST",
Path = "/items/123?filter=active",
Payload = body
},
CancellationToken.None);
response.StatusCode.Should().Be(200);
var dto = JsonSerializer.Deserialize<GetItemResponse>(response.Payload.Span, JsonOptions);
dto.Should().NotBeNull();
dto!.Id.Should().Be(123);
dto.Filter.Should().Be("active");
}
private static ILogger<T> CreateLogger<T>()
{
var factory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.None));
return factory.CreateLogger<T>();
}
private sealed class GetItemRequest
{
public int Id { get; set; }
public string? Filter { get; set; }
}
private sealed class GetItemResponse
{
public int Id { get; set; }
public string? Filter { get; set; }
}
private sealed class GetItemHandler : IStellaEndpoint<GetItemRequest, GetItemResponse>
{
public Task<GetItemResponse> HandleAsync(GetItemRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(new GetItemResponse
{
Id = request.Id,
Filter = request.Filter
});
}
}
}

View File

@@ -0,0 +1,137 @@
using StellaOps.Microservice;
using StellaOps.Router.Common.Enums;
using Xunit;
namespace StellaOps.Microservice.Tests;
public class StellaMicroserviceOptionsTests
{
[Fact]
public void StellaMicroserviceOptions_CanBeCreated()
{
// Arrange & Act
var options = new StellaMicroserviceOptions
{
ServiceName = "test-service",
Version = "1.0.0",
Region = "eu1"
};
// Assert
Assert.Equal("test-service", options.ServiceName);
Assert.Equal("1.0.0", options.Version);
Assert.Equal("eu1", options.Region);
Assert.NotEmpty(options.InstanceId);
}
[Fact]
public void RouterEndpointConfig_CanBeCreated()
{
// Arrange & Act
var config = new RouterEndpointConfig
{
Host = "localhost",
Port = 5000,
TransportType = TransportType.Tcp
};
// Assert
Assert.Equal("localhost", config.Host);
Assert.Equal(5000, config.Port);
Assert.Equal(TransportType.Tcp, config.TransportType);
}
[Fact]
public void Validate_ThrowsIfServiceNameEmpty()
{
// Arrange
var options = new StellaMicroserviceOptions
{
ServiceName = "",
Version = "1.0.0",
Region = "eu1"
};
// Act & Assert
Assert.Throws<InvalidOperationException>(() => options.Validate());
}
[Fact]
public void Validate_ThrowsIfVersionInvalid()
{
// Arrange
var options = new StellaMicroserviceOptions
{
ServiceName = "test",
Version = "not-semver",
Region = "eu1"
};
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("not valid semver", ex.Message);
}
[Fact]
public void Validate_ThrowsIfNoRouters()
{
// Arrange
var options = new StellaMicroserviceOptions
{
ServiceName = "test",
Version = "1.0.0",
Region = "eu1"
};
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("router endpoint is required", ex.Message);
}
[Fact]
public void Validate_AcceptsValidSemver()
{
// Arrange
var options = new StellaMicroserviceOptions
{
ServiceName = "test",
Version = "1.0.0",
Region = "eu1",
Routers = [new RouterEndpointConfig { Host = "localhost", Port = 5000 }]
};
// Act & Assert - no exception
options.Validate();
}
[Fact]
public void Validate_AcceptsSemverWithPrerelease()
{
// Arrange
var options = new StellaMicroserviceOptions
{
ServiceName = "test",
Version = "2.1.0-beta.1",
Region = "eu1",
Routers = [new RouterEndpointConfig { Host = "localhost", Port = 5000 }]
};
// Act & Assert - no exception
options.Validate();
}
[Fact]
public void DefaultHeartbeatInterval_Is10Seconds()
{
// Arrange & Act
var options = new StellaMicroserviceOptions
{
ServiceName = "test",
Version = "1.0.0",
Region = "eu1"
};
// Assert
Assert.Equal(TimeSpan.FromSeconds(10), options.HeartbeatInterval);
}
}

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<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>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\__Libraries\StellaOps.Microservice\StellaOps.Microservice.csproj" />
</ItemGroup>
</Project>

View 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();
}
}