namespace StellaOps.Router.Common.Tests; /// /// Unit tests for . /// 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 }