Add unit tests and logging infrastructure for InMemory and RabbitMQ transports
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented RecordingLogger and RecordingLoggerFactory for capturing log entries in tests. - Added unit tests for InMemoryChannel, covering constructor behavior, property assignments, channel communication, and disposal. - Created InMemoryTransportOptionsTests to validate default values and customizable options for InMemory transport. - Developed RabbitMqFrameProtocolTests to ensure correct parsing and property creation for RabbitMQ frames. - Added RabbitMqTransportOptionsTests to verify default settings and customization options for RabbitMQ transport. - Updated project files for testing libraries and dependencies.
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
using System.Text;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Frames;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
namespace StellaOps.Router.Common.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="FrameConverter"/>.
|
||||
/// </summary>
|
||||
public sealed class FrameConverterTests
|
||||
{
|
||||
#region ToFrame (RequestFrame) Tests
|
||||
|
||||
[Fact]
|
||||
public void ToFrame_RequestFrame_ReturnsFrameWithRequestType()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequestFrame();
|
||||
|
||||
// Act
|
||||
var frame = FrameConverter.ToFrame(request);
|
||||
|
||||
// Assert
|
||||
frame.Type.Should().Be(FrameType.Request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToFrame_RequestFrame_SetsCorrelationIdFromRequest()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequestFrame(correlationId: "test-correlation-123");
|
||||
|
||||
// Act
|
||||
var frame = FrameConverter.ToFrame(request);
|
||||
|
||||
// Assert
|
||||
frame.CorrelationId.Should().Be("test-correlation-123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToFrame_RequestFrame_UsesRequestIdWhenCorrelationIdIsNull()
|
||||
{
|
||||
// Arrange
|
||||
var request = new RequestFrame
|
||||
{
|
||||
RequestId = "request-id-456",
|
||||
CorrelationId = null,
|
||||
Method = "GET",
|
||||
Path = "/test"
|
||||
};
|
||||
|
||||
// Act
|
||||
var frame = FrameConverter.ToFrame(request);
|
||||
|
||||
// Assert
|
||||
frame.CorrelationId.Should().Be("request-id-456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToFrame_RequestFrame_SerializesPayload()
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateTestRequestFrame();
|
||||
|
||||
// Act
|
||||
var frame = FrameConverter.ToFrame(request);
|
||||
|
||||
// Assert
|
||||
frame.Payload.Length.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ToRequestFrame Tests
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_ValidRequestFrame_ReturnsRequestFrame()
|
||||
{
|
||||
// Arrange
|
||||
var originalRequest = CreateTestRequestFrame();
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_WrongFrameType_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var frame = new Frame
|
||||
{
|
||||
Type = FrameType.Response,
|
||||
CorrelationId = "test",
|
||||
Payload = Array.Empty<byte>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_InvalidJson_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var frame = new Frame
|
||||
{
|
||||
Type = FrameType.Request,
|
||||
CorrelationId = "test",
|
||||
Payload = Encoding.UTF8.GetBytes("invalid json {{{")
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesRequestId()
|
||||
{
|
||||
// Arrange
|
||||
var originalRequest = CreateTestRequestFrame(requestId: "unique-request-id");
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.RequestId.Should().Be("unique-request-id");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesMethod()
|
||||
{
|
||||
// Arrange
|
||||
var originalRequest = CreateTestRequestFrame(method: "DELETE");
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.Method.Should().Be("DELETE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesPath()
|
||||
{
|
||||
// Arrange
|
||||
var originalRequest = CreateTestRequestFrame(path: "/api/users/123");
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.Path.Should().Be("/api/users/123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var headers = new Dictionary<string, string>
|
||||
{
|
||||
["Content-Type"] = "application/json",
|
||||
["X-Custom-Header"] = "custom-value"
|
||||
};
|
||||
var originalRequest = new RequestFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
Method = "POST",
|
||||
Path = "/test",
|
||||
Headers = headers
|
||||
};
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.Headers.Should().ContainKey("Content-Type");
|
||||
result.Headers["Content-Type"].Should().Be("application/json");
|
||||
result.Headers["X-Custom-Header"].Should().Be("custom-value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesPayload()
|
||||
{
|
||||
// Arrange
|
||||
var payloadBytes = Encoding.UTF8.GetBytes("{\"key\":\"value\"}");
|
||||
var originalRequest = new RequestFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
Method = "POST",
|
||||
Path = "/test",
|
||||
Payload = payloadBytes
|
||||
};
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.Payload.ToArray().Should().BeEquivalentTo(payloadBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesTimeoutSeconds()
|
||||
{
|
||||
// Arrange
|
||||
var originalRequest = new RequestFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
Method = "GET",
|
||||
Path = "/test",
|
||||
TimeoutSeconds = 60
|
||||
};
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.TimeoutSeconds.Should().Be(60);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_RoundTrip_PreservesSupportsStreaming()
|
||||
{
|
||||
// Arrange
|
||||
var originalRequest = new RequestFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
Method = "GET",
|
||||
Path = "/test",
|
||||
SupportsStreaming = true
|
||||
};
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.SupportsStreaming.Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ToFrame (ResponseFrame) Tests
|
||||
|
||||
[Fact]
|
||||
public void ToFrame_ResponseFrame_ReturnsFrameWithResponseType()
|
||||
{
|
||||
// Arrange
|
||||
var response = CreateTestResponseFrame();
|
||||
|
||||
// Act
|
||||
var frame = FrameConverter.ToFrame(response);
|
||||
|
||||
// Assert
|
||||
frame.Type.Should().Be(FrameType.Response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToFrame_ResponseFrame_SetsCorrelationIdToRequestId()
|
||||
{
|
||||
// Arrange
|
||||
var response = CreateTestResponseFrame(requestId: "req-123");
|
||||
|
||||
// Act
|
||||
var frame = FrameConverter.ToFrame(response);
|
||||
|
||||
// Assert
|
||||
frame.CorrelationId.Should().Be("req-123");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ToResponseFrame Tests
|
||||
|
||||
[Fact]
|
||||
public void ToResponseFrame_ValidResponseFrame_ReturnsResponseFrame()
|
||||
{
|
||||
// Arrange
|
||||
var originalResponse = CreateTestResponseFrame();
|
||||
var frame = FrameConverter.ToFrame(originalResponse);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToResponseFrame(frame);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToResponseFrame_WrongFrameType_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var frame = new Frame
|
||||
{
|
||||
Type = FrameType.Request,
|
||||
CorrelationId = "test",
|
||||
Payload = Array.Empty<byte>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToResponseFrame(frame);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToResponseFrame_InvalidJson_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var frame = new Frame
|
||||
{
|
||||
Type = FrameType.Response,
|
||||
CorrelationId = "test",
|
||||
Payload = Encoding.UTF8.GetBytes("not valid json")
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToResponseFrame(frame);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToResponseFrame_RoundTrip_PreservesRequestId()
|
||||
{
|
||||
// Arrange
|
||||
var originalResponse = CreateTestResponseFrame(requestId: "original-req-id");
|
||||
var frame = FrameConverter.ToFrame(originalResponse);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToResponseFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.RequestId.Should().Be("original-req-id");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToResponseFrame_RoundTrip_PreservesStatusCode()
|
||||
{
|
||||
// Arrange
|
||||
var originalResponse = CreateTestResponseFrame(statusCode: 404);
|
||||
var frame = FrameConverter.ToFrame(originalResponse);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToResponseFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.StatusCode.Should().Be(404);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToResponseFrame_RoundTrip_PreservesHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var headers = new Dictionary<string, string>
|
||||
{
|
||||
["Content-Type"] = "application/json",
|
||||
["Cache-Control"] = "no-cache"
|
||||
};
|
||||
var originalResponse = new ResponseFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
StatusCode = 200,
|
||||
Headers = headers
|
||||
};
|
||||
var frame = FrameConverter.ToFrame(originalResponse);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToResponseFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.Headers["Content-Type"].Should().Be("application/json");
|
||||
result.Headers["Cache-Control"].Should().Be("no-cache");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToResponseFrame_RoundTrip_PreservesPayload()
|
||||
{
|
||||
// Arrange
|
||||
var payloadBytes = Encoding.UTF8.GetBytes("{\"result\":\"success\"}");
|
||||
var originalResponse = new ResponseFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
StatusCode = 200,
|
||||
Payload = payloadBytes
|
||||
};
|
||||
var frame = FrameConverter.ToFrame(originalResponse);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToResponseFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.Payload.ToArray().Should().BeEquivalentTo(payloadBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToResponseFrame_RoundTrip_PreservesHasMoreChunks()
|
||||
{
|
||||
// Arrange
|
||||
var originalResponse = new ResponseFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
StatusCode = 200,
|
||||
HasMoreChunks = true
|
||||
};
|
||||
var frame = FrameConverter.ToFrame(originalResponse);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToResponseFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.HasMoreChunks.Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_EmptyPayload_ReturnsEmptyPayload()
|
||||
{
|
||||
// Arrange
|
||||
var originalRequest = new RequestFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
Method = "GET",
|
||||
Path = "/test",
|
||||
Payload = Array.Empty<byte>()
|
||||
};
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.Payload.IsEmpty.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRequestFrame_NullHeaders_ReturnsEmptyHeaders()
|
||||
{
|
||||
// Arrange
|
||||
var originalRequest = new RequestFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
Method = "GET",
|
||||
Path = "/test"
|
||||
};
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.Headers.Should().NotBeNull();
|
||||
result.Headers.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToResponseFrame_EmptyPayload_ReturnsEmptyPayload()
|
||||
{
|
||||
// Arrange
|
||||
var originalResponse = new ResponseFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
StatusCode = 204,
|
||||
Payload = Array.Empty<byte>()
|
||||
};
|
||||
var frame = FrameConverter.ToFrame(originalResponse);
|
||||
|
||||
// Act
|
||||
var result = FrameConverter.ToResponseFrame(frame);
|
||||
|
||||
// Assert
|
||||
result!.Payload.IsEmpty.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToFrame_LargePayload_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var largePayload = new byte[1024 * 1024]; // 1MB
|
||||
Random.Shared.NextBytes(largePayload);
|
||||
var originalRequest = new RequestFrame
|
||||
{
|
||||
RequestId = "test-id",
|
||||
Method = "POST",
|
||||
Path = "/upload",
|
||||
Payload = largePayload
|
||||
};
|
||||
|
||||
// Act
|
||||
var frame = FrameConverter.ToFrame(originalRequest);
|
||||
var result = FrameConverter.ToRequestFrame(frame);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Payload.ToArray().Should().BeEquivalentTo(largePayload);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static RequestFrame CreateTestRequestFrame(
|
||||
string? requestId = null,
|
||||
string? correlationId = null,
|
||||
string method = "GET",
|
||||
string path = "/test")
|
||||
{
|
||||
return new RequestFrame
|
||||
{
|
||||
RequestId = requestId ?? Guid.NewGuid().ToString("N"),
|
||||
CorrelationId = correlationId,
|
||||
Method = method,
|
||||
Path = path
|
||||
};
|
||||
}
|
||||
|
||||
private static ResponseFrame CreateTestResponseFrame(
|
||||
string? requestId = null,
|
||||
int statusCode = 200)
|
||||
{
|
||||
return new ResponseFrame
|
||||
{
|
||||
RequestId = requestId ?? Guid.NewGuid().ToString("N"),
|
||||
StatusCode = statusCode
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
namespace StellaOps.Router.Common.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="PathMatcher"/>.
|
||||
/// </summary>
|
||||
public sealed class PathMatcherTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsTemplate()
|
||||
{
|
||||
// Arrange & Act
|
||||
var matcher = new PathMatcher("/api/users/{id}");
|
||||
|
||||
// Assert
|
||||
matcher.Template.Should().Be("/api/users/{id}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultsCaseInsensitive()
|
||||
{
|
||||
// Arrange & Act
|
||||
var matcher = new PathMatcher("/api/Users");
|
||||
|
||||
// Assert
|
||||
matcher.IsMatch("/api/users").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CaseSensitive_DoesNotMatchDifferentCase()
|
||||
{
|
||||
// Arrange & Act
|
||||
var matcher = new PathMatcher("/api/Users", caseInsensitive: false);
|
||||
|
||||
// Assert
|
||||
matcher.IsMatch("/api/users").Should().BeFalse();
|
||||
matcher.IsMatch("/api/Users").Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsMatch Tests - Exact Paths
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ExactPath_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/health");
|
||||
|
||||
// Act & Assert
|
||||
matcher.IsMatch("/api/health").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ExactPath_TrailingSlash_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/health");
|
||||
|
||||
// Act & Assert
|
||||
matcher.IsMatch("/api/health/").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ExactPath_NoLeadingSlash_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/health");
|
||||
|
||||
// Act & Assert
|
||||
matcher.IsMatch("api/health").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_DifferentPath_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/health");
|
||||
|
||||
// Act & Assert
|
||||
matcher.IsMatch("/api/status").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_PartialPath_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/list");
|
||||
|
||||
// Act & Assert
|
||||
matcher.IsMatch("/api/users").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_LongerPath_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users");
|
||||
|
||||
// Act & Assert
|
||||
matcher.IsMatch("/api/users/list").Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsMatch Tests - Case Sensitivity
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_CaseInsensitive_MatchesMixedCase()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users", caseInsensitive: true);
|
||||
|
||||
// Act & Assert
|
||||
matcher.IsMatch("/API/USERS").Should().BeTrue();
|
||||
matcher.IsMatch("/Api/Users").Should().BeTrue();
|
||||
matcher.IsMatch("/aPi/uSeRs").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_CaseSensitive_OnlyMatchesExactCase()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/Api/Users", caseInsensitive: false);
|
||||
|
||||
// Act & Assert
|
||||
matcher.IsMatch("/Api/Users").Should().BeTrue();
|
||||
matcher.IsMatch("/api/users").Should().BeFalse();
|
||||
matcher.IsMatch("/API/USERS").Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryMatch Tests - Single Parameter
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_SingleParameter_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{id}");
|
||||
|
||||
// Act
|
||||
var result = matcher.TryMatch("/api/users/123", out var parameters);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_SingleParameter_ExtractsParameter()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{id}");
|
||||
|
||||
// Act
|
||||
matcher.TryMatch("/api/users/123", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters.Should().ContainKey("id");
|
||||
parameters["id"].Should().Be("123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_SingleParameter_ExtractsGuidParameter()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{userId}");
|
||||
var guid = Guid.NewGuid().ToString();
|
||||
|
||||
// Act
|
||||
matcher.TryMatch($"/api/users/{guid}", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters["userId"].Should().Be(guid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_SingleParameter_ExtractsStringParameter()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{username}");
|
||||
|
||||
// Act
|
||||
matcher.TryMatch("/api/users/john-doe", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters["username"].Should().Be("john-doe");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryMatch Tests - Multiple Parameters
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_MultipleParameters_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{userId}/posts/{postId}");
|
||||
|
||||
// Act
|
||||
var result = matcher.TryMatch("/api/users/123/posts/456", out _);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_MultipleParameters_ExtractsAllParameters()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{userId}/posts/{postId}");
|
||||
|
||||
// Act
|
||||
matcher.TryMatch("/api/users/user-1/posts/post-2", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters.Should().ContainKey("userId");
|
||||
parameters.Should().ContainKey("postId");
|
||||
parameters["userId"].Should().Be("user-1");
|
||||
parameters["postId"].Should().Be("post-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_ThreeParameters_ExtractsAllParameters()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/org/{orgId}/users/{userId}/roles/{roleId}");
|
||||
|
||||
// Act
|
||||
matcher.TryMatch("/api/org/acme/users/john/roles/admin", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters.Should().HaveCount(3);
|
||||
parameters["orgId"].Should().Be("acme");
|
||||
parameters["userId"].Should().Be("john");
|
||||
parameters["roleId"].Should().Be("admin");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryMatch Tests - Non-Matching
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_NonMatchingPath_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{id}");
|
||||
|
||||
// Act
|
||||
var result = matcher.TryMatch("/api/posts/123", out var parameters);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
parameters.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_MissingParameter_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{id}/posts/{postId}");
|
||||
|
||||
// Act
|
||||
var result = matcher.TryMatch("/api/users/123/posts", out var parameters);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_ExtraSegment_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{id}");
|
||||
|
||||
// Act
|
||||
var result = matcher.TryMatch("/api/users/123/extra", out _);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryMatch Tests - Path Normalization
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_TrailingSlash_Matches()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{id}");
|
||||
|
||||
// Act
|
||||
var result = matcher.TryMatch("/api/users/123/", out var parameters);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
parameters["id"].Should().Be("123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_NoLeadingSlash_Matches()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{id}");
|
||||
|
||||
// Act
|
||||
var result = matcher.TryMatch("api/users/123", out var parameters);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
parameters["id"].Should().Be("123");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryMatch Tests - Parameter Type Constraints
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_ParameterWithTypeConstraint_ExtractsParameterName()
|
||||
{
|
||||
// Arrange
|
||||
// The PathMatcher ignores type constraints but still extracts the parameter
|
||||
var matcher = new PathMatcher("/api/users/{id:int}");
|
||||
|
||||
// Act
|
||||
matcher.TryMatch("/api/users/123", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters.Should().ContainKey("id");
|
||||
parameters["id"].Should().Be("123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_ParameterWithGuidConstraint_ExtractsParameterName()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{id:guid}");
|
||||
|
||||
// Act
|
||||
matcher.TryMatch("/api/users/abc-123", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters.Should().ContainKey("id");
|
||||
parameters["id"].Should().Be("abc-123");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_RootPath_Matches()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/");
|
||||
|
||||
// Act
|
||||
var result = matcher.TryMatch("/", out var parameters);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
parameters.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_SingleSegmentWithParameter_Matches()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/{id}");
|
||||
|
||||
// Act
|
||||
var result = matcher.TryMatch("/test-value", out var parameters);
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
parameters["id"].Should().Be("test-value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_EmptyPath_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/");
|
||||
|
||||
// Act
|
||||
var result = matcher.IsMatch("");
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_ParameterWithHyphen_Extracts()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{user-id}");
|
||||
|
||||
// Act
|
||||
matcher.TryMatch("/api/users/123", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters.Should().ContainKey("user-id");
|
||||
parameters["user-id"].Should().Be("123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_ParameterWithUnderscore_Extracts()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/users/{user_id}");
|
||||
|
||||
// Act
|
||||
matcher.TryMatch("/api/users/456", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters.Should().ContainKey("user_id");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_SpecialCharactersInPath_Matches()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/api/search/{query}");
|
||||
|
||||
// Act
|
||||
matcher.TryMatch("/api/search/hello-world_test.123", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters["query"].Should().Be("hello-world_test.123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ComplexRealWorldPath_Matches()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/v1/organizations/{orgId}/projects/{projectId}/scans/{scanId}/vulnerabilities");
|
||||
|
||||
// Act
|
||||
var result = matcher.IsMatch("/v1/organizations/acme-corp/projects/webapp/scans/scan-2024-001/vulnerabilities");
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMatch_ComplexRealWorldPath_ExtractsAllParameters()
|
||||
{
|
||||
// Arrange
|
||||
var matcher = new PathMatcher("/v1/organizations/{orgId}/projects/{projectId}/scans/{scanId}");
|
||||
|
||||
// Act
|
||||
matcher.TryMatch("/v1/organizations/acme-corp/projects/webapp/scans/scan-2024-001", out var parameters);
|
||||
|
||||
// Assert
|
||||
parameters["orgId"].Should().Be("acme-corp");
|
||||
parameters["projectId"].Should().Be("webapp");
|
||||
parameters["scanId"].Should().Be("scan-2024-001");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<!-- Suppress CA2255 from OpenSSL auto-init shim included via Directory.Build.props -->
|
||||
<NoWarn>$(NoWarn);CA2255</NoWarn>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>StellaOps.Router.Common.Tests</RootNamespace>
|
||||
<!-- Disable Concelier test infrastructure (Mongo2Go, etc.) since not needed for Router tests -->
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Test SDK packages come from Directory.Build.props -->
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Router.Testing\StellaOps.Router.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user