diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs index ec7f0cfdb..7ef6b2a0c 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiHeaderDeserializer.cs @@ -87,6 +87,12 @@ internal static partial class OpenApiV3Deserializer "schema", (o, n, t) => o.Schema = LoadSchema(n, t) }, + { + "content", (o, n, t) => + { + o.Content = n.CreateMap(LoadMediaType, t); + } + }, { "examples", (o, n, t) => o.Examples = n.CreateMap(LoadExample, t) diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs index 6ed2fe97f..208899b54 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiHeaderDeserializer.cs @@ -87,6 +87,12 @@ internal static partial class OpenApiV31Deserializer o.Schema = LoadSchema(n, t); } }, + { + "content", (o, n, t) => + { + o.Content = n.CreateMap(LoadMediaType, t); + } + }, { "examples", (o, n, t) => { diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiHeaderTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiHeaderTests.cs new file mode 100644 index 000000000..60ec248a0 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiHeaderTests.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.OpenApi.Reader; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V31Tests +{ + [Collection("DefaultSettings")] + public class OpenApiHeaderTests + { + private const string SampleFolderPath = "V31Tests/Samples/OpenApiHeader/"; + + [Fact] + public async Task ParseBasicHeaderShouldSucceed() + { + // Arrange + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "basicHeader.yaml")); + + // Act + var header = await OpenApiModelFactory.LoadAsync(stream, OpenApiSpecVersion.OpenApi3_1, new(), settings: SettingsFixture.ReaderSettings); + + // Assert + Assert.Equivalent( + new OpenApiHeader + { + Description = "The number of allowed requests in the current period", + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Integer + } + }, header); + } + + [Fact] + public async Task ParseHeaderWithContentShouldSucceed() + { + // Arrange + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "headerWithContent.yaml")); + + // Act + var header = await OpenApiModelFactory.LoadAsync(stream, OpenApiSpecVersion.OpenApi3_1, new(), settings: SettingsFixture.ReaderSettings); + + // Assert + Assert.Equivalent( + new OpenApiHeader + { + Description = "A complex header with content", + Content = new Dictionary() + { + ["application/json"] = new() + { + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["timestamp"] = new OpenApiSchema() + { + Type = JsonSchemaType.String, + Format = "date-time" + }, + ["value"] = new OpenApiSchema() + { + Type = JsonSchemaType.Integer + } + } + } + } + } + }, header); + } + + [Fact] + public async Task ParseHeaderWithMultipleContentTypesShouldSucceed() + { + // Arrange + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "headerWithMultipleContentTypes.yaml")); + + // Act + var header = await OpenApiModelFactory.LoadAsync(stream, OpenApiSpecVersion.OpenApi3_1, new(), settings: SettingsFixture.ReaderSettings); + + // Assert + Assert.Equivalent( + new OpenApiHeader + { + Description = "A header that accepts multiple content types", + Content = new Dictionary() + { + ["application/json"] = new() + { + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["data"] = new OpenApiSchema() + { + Type = JsonSchemaType.String + } + } + } + }, + ["text/plain"] = new() + { + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.String + } + } + } + }, header); + } + + [Fact] + public async Task ParseHeaderWithStyleAndContentShouldPreferContent() + { + // Arrange + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "headerWithStyleAndContent.yaml")); + + // Act + var header = await OpenApiModelFactory.LoadAsync(stream, OpenApiSpecVersion.OpenApi3_1, new(), settings: SettingsFixture.ReaderSettings); + + // Assert + // Both content and style can be present, content takes precedence for serialization behavior + Assert.NotNull(header.Content); + Assert.Single(header.Content); + Assert.True(header.Content.ContainsKey("application/json")); + Assert.Equal(ParameterStyle.Simple, header.Style); // Style can still be present + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/basicHeader.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/basicHeader.yaml new file mode 100644 index 000000000..015804759 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/basicHeader.yaml @@ -0,0 +1,3 @@ +description: "The number of allowed requests in the current period" +schema: + type: integer \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/headerWithContent.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/headerWithContent.yaml new file mode 100644 index 000000000..dc2df9ffd --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/headerWithContent.yaml @@ -0,0 +1,11 @@ +description: "A complex header with content" +content: + application/json: + schema: + type: object + properties: + timestamp: + type: string + format: date-time + value: + type: integer \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/headerWithMultipleContentTypes.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/headerWithMultipleContentTypes.yaml new file mode 100644 index 000000000..82c74ea60 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/headerWithMultipleContentTypes.yaml @@ -0,0 +1,11 @@ +description: "A header that accepts multiple content types" +content: + application/json: + schema: + type: object + properties: + data: + type: string + text/plain: + schema: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/headerWithStyleAndContent.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/headerWithStyleAndContent.yaml new file mode 100644 index 000000000..fbe176683 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiHeader/headerWithStyleAndContent.yaml @@ -0,0 +1,11 @@ +description: "A header with both style and content (content should take precedence)" +style: simple +schema: + type: string +content: + application/json: + schema: + type: object + properties: + value: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiHeaderTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiHeaderTests.cs new file mode 100644 index 000000000..ba0ab49d3 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiHeaderTests.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.OpenApi.Reader; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V3Tests +{ + [Collection("DefaultSettings")] + public class OpenApiHeaderTests + { + private const string SampleFolderPath = "V3Tests/Samples/OpenApiHeader/"; + + [Fact] + public async Task ParseBasicHeaderShouldSucceed() + { + // Arrange + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "basicHeader.yaml")); + + // Act + var header = await OpenApiModelFactory.LoadAsync(stream, OpenApiSpecVersion.OpenApi3_0, new(), settings: SettingsFixture.ReaderSettings); + + // Assert + Assert.Equivalent( + new OpenApiHeader + { + Description = "The number of allowed requests in the current period", + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Integer + } + }, header); + } + + [Fact] + public async Task ParseHeaderWithContentShouldSucceed() + { + // Arrange + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "headerWithContent.yaml")); + + // Act + var header = await OpenApiModelFactory.LoadAsync(stream, OpenApiSpecVersion.OpenApi3_0, new(), settings: SettingsFixture.ReaderSettings); + + // Assert + Assert.Equivalent( + new OpenApiHeader + { + Description = "A complex header with content", + Content = new Dictionary() + { + ["application/json"] = new() + { + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["timestamp"] = new OpenApiSchema() + { + Type = JsonSchemaType.String, + Format = "date-time" + }, + ["value"] = new OpenApiSchema() + { + Type = JsonSchemaType.Integer + } + } + } + } + } + }, header); + } + + [Fact] + public async Task ParseHeaderWithMultipleContentTypesShouldSucceed() + { + // Arrange + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "headerWithMultipleContentTypes.yaml")); + + // Act + var header = await OpenApiModelFactory.LoadAsync(stream, OpenApiSpecVersion.OpenApi3_0, new(), settings: SettingsFixture.ReaderSettings); + + // Assert + Assert.Equivalent( + new OpenApiHeader + { + Description = "A header that accepts multiple content types", + Content = new Dictionary() + { + ["application/json"] = new() + { + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["data"] = new OpenApiSchema() + { + Type = JsonSchemaType.String + } + } + } + }, + ["text/plain"] = new() + { + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.String + } + } + } + }, header); + } + + [Fact] + public async Task ParseHeaderWithStyleAndContentShouldPreferContent() + { + // Arrange + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "headerWithStyleAndContent.yaml")); + + // Act + var header = await OpenApiModelFactory.LoadAsync(stream, OpenApiSpecVersion.OpenApi3_0, new(), settings: SettingsFixture.ReaderSettings); + + // Assert + // Both content and style can be present, content takes precedence for serialization behavior + Assert.NotNull(header.Content); + Assert.Single(header.Content); + Assert.True(header.Content.ContainsKey("application/json")); + Assert.Equal(ParameterStyle.Simple, header.Style); // Style can still be present + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/basicHeader.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/basicHeader.yaml new file mode 100644 index 000000000..015804759 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/basicHeader.yaml @@ -0,0 +1,3 @@ +description: "The number of allowed requests in the current period" +schema: + type: integer \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/headerWithContent.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/headerWithContent.yaml new file mode 100644 index 000000000..dc2df9ffd --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/headerWithContent.yaml @@ -0,0 +1,11 @@ +description: "A complex header with content" +content: + application/json: + schema: + type: object + properties: + timestamp: + type: string + format: date-time + value: + type: integer \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/headerWithMultipleContentTypes.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/headerWithMultipleContentTypes.yaml new file mode 100644 index 000000000..82c74ea60 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/headerWithMultipleContentTypes.yaml @@ -0,0 +1,11 @@ +description: "A header that accepts multiple content types" +content: + application/json: + schema: + type: object + properties: + data: + type: string + text/plain: + schema: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/headerWithStyleAndContent.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/headerWithStyleAndContent.yaml new file mode 100644 index 000000000..fbe176683 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiHeader/headerWithStyleAndContent.yaml @@ -0,0 +1,11 @@ +description: "A header with both style and content (content should take precedence)" +style: simple +schema: + type: string +content: + application/json: + schema: + type: object + properties: + value: + type: string \ No newline at end of file