Skip to content
Open
11 changes: 7 additions & 4 deletions src/Microsoft.OpenApi/Models/OpenApiPaths.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
Expand All @@ -14,12 +16,13 @@ public class OpenApiPaths : OpenApiExtensibleDictionary<IOpenApiPathItem>
public OpenApiPaths() { }

/// <summary>
/// Initializes a copy of <see cref="OpenApiPaths"/> object
/// Initializes a copy of <see cref="OpenApiPaths"/> object.
/// </summary>
/// <param name="paths">The <see cref="OpenApiPaths"/>.</param>
/// <remarks>
/// 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.
/// </remarks>
public OpenApiPaths(OpenApiPaths paths) : base(dictionary: paths) { }
public OpenApiPaths(OpenApiPaths paths) : base(dictionary: paths, extensions: paths?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(paths.Extensions) : null) { }
}
}
14 changes: 10 additions & 4 deletions src/Microsoft.OpenApi/Models/OpenApiResponses.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
Expand All @@ -14,9 +16,13 @@ public class OpenApiResponses : OpenApiExtensibleDictionary<IOpenApiResponse>
public OpenApiResponses() { }

/// <summary>
/// Initializes a copy of <see cref="OpenApiResponses"/> object
/// Initializes a copy of <see cref="OpenApiResponses"/> object.
/// </summary>
/// <param name="openApiResponses">The <see cref="OpenApiResponses"/></param>
public OpenApiResponses(OpenApiResponses openApiResponses) : base(dictionary: openApiResponses) { }
/// <param name="openApiResponses">The <see cref="OpenApiResponses"/>.</param>
/// <remarks>
/// 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.
/// </remarks>
public OpenApiResponses(OpenApiResponses openApiResponses) : base(dictionary: openApiResponses, extensions: openApiResponses?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(openApiResponses.Extensions) : null) { }
}
}
34 changes: 33 additions & 1 deletion test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -2753,5 +2753,37 @@ public void GetOperationById_ThrowsOnNullOrEmptyId()
Assert.Throws<ArgumentNullException>(() => doc.GetOperationById(null!));
Assert.Throws<ArgumentNullException>(() => 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<string, IOpenApiExtension>
{
["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"));
}
}
}
32 changes: 32 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, IOpenApiExtension>
{
["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"));
}
}
}
101 changes: 101 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, IOpenApiExtension>
{
["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<string, IOpenApiExtension>
{
["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<string, IOpenApiExtension>()
};

// Act
var copy = new OpenApiPaths(paths);

// Assert
Assert.NotNull(copy.Extensions);
Assert.Empty(copy.Extensions);
}
}
}
101 changes: 101 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, IOpenApiExtension>
{
["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<string, IOpenApiExtension>
{
["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<string, IOpenApiExtension>()
};

// Act
var copy = new OpenApiResponses(responses);

// Assert
Assert.NotNull(copy.Extensions);
Assert.Empty(copy.Extensions);
}
}
}