diff --git a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs index 53a79c8a4..e70f129b9 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiUrlTreeNode.cs @@ -150,6 +150,13 @@ public OpenApiUrlTreeNode Attach(string path, } var segments = path.Split('/'); + if (path.EndsWith("/", StringComparison.OrdinalIgnoreCase)) + { + // Remove the last element, which is empty, and append the trailing slash to the new last element + // This is to support URLs with trailing slashes + Array.Resize(ref segments, segments.Length - 1); + segments[segments.Length - 1] += @"\"; + } return Attach(segments: segments, pathItem: pathItem, diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs index 8d0bb8cd5..4da7a337f 100644 --- a/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiUrlTreeNodeTests.cs @@ -466,5 +466,38 @@ public async Task VerifyDiagramFromSampleOpenAPIAsync() await Verifier.Verify(diagram); } + + public static TheoryData SupportsTrailingSlashesInPathData => new TheoryData + { + // Path, children up to second to leaf, last expected leaf node name, expected leaf node path + { "/cars/{car-id}/build/", ["cars", "{car-id}"], @"build\", @"\cars\{car-id}\build\" }, + { "/cars/", [], @"cars\", @"\cars\" }, + }; + + [Theory] + [MemberData(nameof(SupportsTrailingSlashesInPathData))] + public void SupportsTrailingSlashesInPath(string path, string[] childrenBeforeLastNode, string expectedLeafNodeName, string expectedLeafNodePath) + { + var openApiDocument = new OpenApiDocument + { + Paths = new() + { + [path] = new() + } + }; + + var label = "trailing-slash"; + var rootNode = OpenApiUrlTreeNode.Create(openApiDocument, label); + + var secondToLeafNode = rootNode; + foreach (var childName in childrenBeforeLastNode) + { + secondToLeafNode = secondToLeafNode.Children[childName]; + } + + Assert.True(secondToLeafNode.Children.TryGetValue(expectedLeafNodeName, out var leafNode)); + Assert.Equal(expectedLeafNodePath, leafNode.Path); + Assert.Empty(leafNode.Children); + } } }