diff --git a/src/Microsoft.OpenApi/Models/OpenApiPaths.cs b/src/Microsoft.OpenApi/Models/OpenApiPaths.cs index 1aa163e3c..d79c95320 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiPaths.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiPaths.cs @@ -1,6 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Collections.Generic; + namespace Microsoft.OpenApi { /// @@ -14,12 +16,13 @@ public class OpenApiPaths : OpenApiExtensibleDictionary public OpenApiPaths() { } /// - /// Initializes a copy of object + /// Initializes a copy of object. /// /// The . /// - /// This creates a shallow copy, the path items are the same reference as in the provided parameter. + /// This creates a shallow copy; the path items are the same reference as in the provided parameter, + /// and specification extensions are copied into a new dictionary. /// - public OpenApiPaths(OpenApiPaths paths) : base(dictionary: paths) { } + public OpenApiPaths(OpenApiPaths paths) : base(dictionary: paths, extensions: paths?.Extensions != null ? new Dictionary(paths.Extensions) : null) { } } } diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponses.cs b/src/Microsoft.OpenApi/Models/OpenApiResponses.cs index 63a144a2a..f510c9cd5 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiResponses.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiResponses.cs @@ -1,6 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Collections.Generic; + namespace Microsoft.OpenApi { /// @@ -14,9 +16,13 @@ public class OpenApiResponses : OpenApiExtensibleDictionary public OpenApiResponses() { } /// - /// Initializes a copy of object + /// Initializes a copy of object. /// - /// The - public OpenApiResponses(OpenApiResponses openApiResponses) : base(dictionary: openApiResponses) { } + /// The . + /// + /// This creates a shallow copy; the responses are the same reference as in the provided parameter, + /// and specification extensions are copied into a new dictionary. + /// + public OpenApiResponses(OpenApiResponses openApiResponses) : base(dictionary: openApiResponses, extensions: openApiResponses?.Extensions != null ? new Dictionary(openApiResponses.Extensions) : null) { } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index 3abfb42fd..774d9ff2a 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -2753,5 +2753,37 @@ public void GetOperationById_ThrowsOnNullOrEmptyId() Assert.Throws(() => doc.GetOperationById(null!)); Assert.Throws(() => doc.GetOperationById(string.Empty)); } + + [Fact] + public void OpenApiDocumentCopyConstructorPreservesPathsExtensions() + { + // Arrange + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" }, + Paths = new OpenApiPaths + { + ["/items"] = new OpenApiPathItem(), + Extensions = new Dictionary + { + ["x-paths-summary"] = new JsonNodeExtension("Paths level extension") + } + } + }; + + // Act + var copy = new OpenApiDocument(document); + + // Assert + Assert.NotNull(copy.Paths); + Assert.NotNull(copy.Paths.Extensions); + Assert.Single(copy.Paths.Extensions); + Assert.True(copy.Paths.Extensions.ContainsKey("x-paths-summary")); + + // Verify isolation + copy.Paths.Extensions["x-isolated"] = new JsonNodeExtension("isolated"); + Assert.Single(document.Paths.Extensions); + Assert.False(document.Paths.Extensions.ContainsKey("x-isolated")); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs index b43ef7b53..8a9af17cc 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs @@ -995,5 +995,37 @@ public void OpenApiOperationCopyConstructorPreservesImmutableSortedSet() var copyNames = copy.Tags.Select(t => t.Name); Assert.Equal(["tagC", "tagB", "tagA"], copyNames); } + + [Fact] + public void OpenApiOperationCopyConstructorPreservesResponsesExtensions() + { + // Arrange + var operation = new OpenApiOperation + { + OperationId = "getItem", + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse { Description = "Success" }, + Extensions = new Dictionary + { + ["x-responses-meta"] = new JsonNodeExtension("Metadata for responses") + } + } + }; + + // Act + var copy = new OpenApiOperation(operation); + + // Assert + Assert.NotNull(copy.Responses); + Assert.NotNull(copy.Responses.Extensions); + Assert.Single(copy.Responses.Extensions); + Assert.True(copy.Responses.Extensions.ContainsKey("x-responses-meta")); + + // Verify isolation + copy.Responses.Extensions["x-isolated"] = new JsonNodeExtension("isolated"); + Assert.Single(operation.Responses.Extensions); + Assert.False(operation.Responses.Extensions.ContainsKey("x-isolated")); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs new file mode 100644 index 000000000..c4ebc20df --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Models +{ + [Collection("DefaultSettings")] + public class OpenApiPathsTests + { + [Fact] + public void CopyConstructorCopiesExtensions() + { + // Arrange + var paths = new OpenApiPaths + { + ["/users"] = new OpenApiPathItem(), + Extensions = new Dictionary + { + ["x-custom-path"] = new JsonNodeExtension("custom-value"), + ["x-rate-limit"] = new JsonNodeExtension(100), + } + }; + + // Act + var copy = new OpenApiPaths(paths); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Equal(2, copy.Extensions.Count); + Assert.True(copy.Extensions.ContainsKey("x-custom-path")); + Assert.True(copy.Extensions.ContainsKey("x-rate-limit")); + } + + [Fact] + public void CopyConstructorIsolatesExtensionsDictionary() + { + // Arrange + var paths = new OpenApiPaths + { + Extensions = new Dictionary + { + ["x-original"] = new JsonNodeExtension("original") + } + }; + + // Act + var copy = new OpenApiPaths(paths); + copy.Extensions["x-copy-only"] = new JsonNodeExtension("copy"); + + // Assert + Assert.Equal(2, copy.Extensions.Count); + Assert.Single(paths.Extensions); + Assert.False(paths.Extensions.ContainsKey("x-copy-only")); + } + + [Fact] + public void CopyConstructorWithNullPathsInitializesEmptyExtensions() + { + // Act + var copy = new OpenApiPaths(null!); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + Assert.Empty(copy); + } + + [Fact] + public void CopyConstructorWithNullExtensionsInitializesEmptyExtensions() + { + // Arrange + var paths = new OpenApiPaths { Extensions = null }; + + // Act + var copy = new OpenApiPaths(paths); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + } + + [Fact] + public void CopyConstructorWithEmptyExtensionsInitializesEmptyExtensions() + { + // Arrange + var paths = new OpenApiPaths + { + Extensions = new Dictionary() + }; + + // Act + var copy = new OpenApiPaths(paths); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs new file mode 100644 index 000000000..35390ccb4 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Models +{ + [Collection("DefaultSettings")] + public class OpenApiResponsesTests + { + [Fact] + public void CopyConstructorCopiesExtensions() + { + // Arrange + var responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse { Description = "OK" }, + Extensions = new Dictionary + { + ["x-response-notes"] = new JsonNodeExtension("all-responses-note"), + ["x-default-code"] = new JsonNodeExtension("200"), + } + }; + + // Act + var copy = new OpenApiResponses(responses); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Equal(2, copy.Extensions.Count); + Assert.True(copy.Extensions.ContainsKey("x-response-notes")); + Assert.True(copy.Extensions.ContainsKey("x-default-code")); + } + + [Fact] + public void CopyConstructorIsolatesExtensionsDictionary() + { + // Arrange + var responses = new OpenApiResponses + { + Extensions = new Dictionary + { + ["x-original"] = new JsonNodeExtension("original") + } + }; + + // Act + var copy = new OpenApiResponses(responses); + copy.Extensions["x-copy-only"] = new JsonNodeExtension("copy"); + + // Assert + Assert.Equal(2, copy.Extensions.Count); + Assert.Single(responses.Extensions); + Assert.False(responses.Extensions.ContainsKey("x-copy-only")); + } + + [Fact] + public void CopyConstructorWithNullResponsesInitializesEmptyExtensions() + { + // Act + var copy = new OpenApiResponses(null!); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + Assert.Empty(copy); + } + + [Fact] + public void CopyConstructorWithNullExtensionsInitializesEmptyExtensions() + { + // Arrange + var responses = new OpenApiResponses { Extensions = null }; + + // Act + var copy = new OpenApiResponses(responses); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + } + + [Fact] + public void CopyConstructorWithEmptyExtensionsInitializesEmptyExtensions() + { + // Arrange + var responses = new OpenApiResponses + { + Extensions = new Dictionary() + }; + + // Act + var copy = new OpenApiResponses(responses); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + } + } +}