From 93f60cc63afe52f8f5bfa62a91da1ccfe9ece091 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 14 Jul 2026 18:21:48 +0000 Subject: [PATCH 1/2] Merge 16826b07e4ab23b50d896dcebc3b8cc56dca33df into 3ef2b9e2306a9398ef921a721027103d04de15d9 --- .../Models/OpenApiSecurityScheme.cs | 21 +++ .../OpenApiComponentsSerializationTests.cs | 2 + .../Models/OpenApiSecuritySchemeTests.cs | 155 ++++++++++++++++++ 3 files changed, 178 insertions(+) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs index 50c2ba545..e3e6f587c 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs @@ -80,6 +80,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version Action callback) { Utils.CheckArgumentNull(writer); + EnsureRequiredPropertiesAreSet(version); writer.WriteStartObject(); @@ -137,6 +138,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version public virtual void SerializeAsV2(IOpenApiWriter writer) { Utils.CheckArgumentNull(writer); + EnsureRequiredPropertiesAreSet(OpenApiSpecVersion.OpenApi2_0); if (Type == SecuritySchemeType.Http && Scheme != OpenApiConstants.Basic) { @@ -200,6 +202,25 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) writer.WriteEndObject(); } + private void EnsureRequiredPropertiesAreSet(OpenApiSpecVersion version) + { + switch (Type) + { + case null: + throw new OpenApiException("The 'type' property is required for security schemes."); + case SecuritySchemeType.ApiKey when string.IsNullOrEmpty(Name): + throw new OpenApiException("The 'name' property is required for apiKey security schemes."); + case SecuritySchemeType.ApiKey when In is null: + throw new OpenApiException("The 'in' property is required for apiKey security schemes."); + case SecuritySchemeType.Http when version >= OpenApiSpecVersion.OpenApi3_0 && string.IsNullOrEmpty(Scheme): + throw new OpenApiException("The 'scheme' property is required for http security schemes."); + case SecuritySchemeType.OAuth2 when Flows is null: + throw new OpenApiException("The 'flows' property is required for oauth2 security schemes."); + case SecuritySchemeType.OpenIdConnect when version >= OpenApiSpecVersion.OpenApi3_0 && OpenIdConnectUrl is null: + throw new OpenApiException("The 'openIdConnectUrl' property is required for openIdConnect security schemes."); + } + } + /// /// Arbitrarily chooses one object from the /// to populate in V2 security scheme. diff --git a/test/Microsoft.OpenApi.Tests/Mocks/OpenApiComponentsSerializationTests.cs b/test/Microsoft.OpenApi.Tests/Mocks/OpenApiComponentsSerializationTests.cs index 144f28500..65b512b21 100644 --- a/test/Microsoft.OpenApi.Tests/Mocks/OpenApiComponentsSerializationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Mocks/OpenApiComponentsSerializationTests.cs @@ -26,6 +26,8 @@ public OpenApiComponentsSerializationTests() _components.Responses["200"] = _responseMock.Object; _components.Parameters["limit"] = _parameterMock.Object; _components.Headers["x-rate-limit"] = _headerMock.Object; + _securitySchemeMock.Object.Type = SecuritySchemeType.Http; + _securitySchemeMock.Object.Scheme = "bearer"; _components.SecuritySchemes["api_key"] = _securitySchemeMock.Object; _components.Links["UserRepositories"] = _linkMock.Object; _components.Callbacks["onData"] = _callbackMock.Object; diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs index 0b69e9de7..e566e1448 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Text.Json.Nodes; using System.Threading.Tasks; using VerifyXunit; using Xunit; @@ -159,6 +160,155 @@ public async Task SerializeApiKeySecuritySchemeAsV3YamlWorks() Assert.Equal(expected, actual); } + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeSecuritySchemeWithoutTypeThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme(); + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'type' property is required for security schemes."); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeApiKeySecuritySchemeWithoutNameThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Query + }; + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'name' property is required for apiKey security schemes."); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeApiKeySecuritySchemeWithoutInThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Name = "parameterName", + Type = SecuritySchemeType.ApiKey + }; + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'in' property is required for apiKey security schemes."); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeHttpSecuritySchemeWithoutSchemeThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http + }; + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'scheme' property is required for http security schemes."); + } + + [Fact] + public async Task SerializeHttpSecuritySchemeWithoutSchemeAsV2DoesNotThrow() + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http + }; + + // Act + var actual = await securityScheme.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi2_0); + + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse("{}"), JsonNode.Parse(actual))); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeOAuth2SecuritySchemeWithoutFlowsThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2 + }; + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'flows' property is required for oauth2 security schemes."); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public Task SerializeOpenIdConnectSecuritySchemeWithoutOpenIdConnectUrlThrows(OpenApiSpecVersion version) + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OpenIdConnect + }; + + // Act & Assert + return AssertRequiredPropertyThrowsAsync( + securityScheme, + version, + "The 'openIdConnectUrl' property is required for openIdConnect security schemes."); + } + + [Fact] + public async Task SerializeOpenIdConnectSecuritySchemeWithoutOpenIdConnectUrlAsV2DoesNotThrow() + { + // Arrange + var securityScheme = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OpenIdConnect + }; + + // Act + var actual = await securityScheme.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi2_0); + + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse("{}"), JsonNode.Parse(actual))); + } + [Fact] public async Task SerializeHttpBasicSecuritySchemeAsV3JsonWorks() { @@ -348,5 +498,10 @@ public async Task SerializeReferencedSecuritySchemeAsV3JsonWithoutReferenceWorks // Assert await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); } + private static async Task AssertRequiredPropertyThrowsAsync(OpenApiSecurityScheme securityScheme, OpenApiSpecVersion version, string message) + { + var exception = await Assert.ThrowsAsync(() => securityScheme.SerializeAsJsonAsync(version)); + Assert.Contains(message, exception.Message); + } } } From cd379d417397a8c5ace6c366a19f402de0d9ede6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 14 Jul 2026 14:47:27 -0400 Subject: [PATCH 2/2] test(library): remove invalid OpenAPI 3.2 inline data Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b7a44557-39ee-4285-886c-7918d3caec2f --- .../Models/OpenApiSecuritySchemeTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs index e566e1448..8c7300aba 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs @@ -164,7 +164,6 @@ public async Task SerializeApiKeySecuritySchemeAsV3YamlWorks() [InlineData(OpenApiSpecVersion.OpenApi2_0)] [InlineData(OpenApiSpecVersion.OpenApi3_0)] [InlineData(OpenApiSpecVersion.OpenApi3_1)] - [InlineData(OpenApiSpecVersion.OpenApi3_2)] public Task SerializeSecuritySchemeWithoutTypeThrows(OpenApiSpecVersion version) { // Arrange @@ -181,7 +180,6 @@ public Task SerializeSecuritySchemeWithoutTypeThrows(OpenApiSpecVersion version) [InlineData(OpenApiSpecVersion.OpenApi2_0)] [InlineData(OpenApiSpecVersion.OpenApi3_0)] [InlineData(OpenApiSpecVersion.OpenApi3_1)] - [InlineData(OpenApiSpecVersion.OpenApi3_2)] public Task SerializeApiKeySecuritySchemeWithoutNameThrows(OpenApiSpecVersion version) { // Arrange @@ -202,7 +200,6 @@ public Task SerializeApiKeySecuritySchemeWithoutNameThrows(OpenApiSpecVersion ve [InlineData(OpenApiSpecVersion.OpenApi2_0)] [InlineData(OpenApiSpecVersion.OpenApi3_0)] [InlineData(OpenApiSpecVersion.OpenApi3_1)] - [InlineData(OpenApiSpecVersion.OpenApi3_2)] public Task SerializeApiKeySecuritySchemeWithoutInThrows(OpenApiSpecVersion version) { // Arrange @@ -222,7 +219,6 @@ public Task SerializeApiKeySecuritySchemeWithoutInThrows(OpenApiSpecVersion vers [Theory] [InlineData(OpenApiSpecVersion.OpenApi3_0)] [InlineData(OpenApiSpecVersion.OpenApi3_1)] - [InlineData(OpenApiSpecVersion.OpenApi3_2)] public Task SerializeHttpSecuritySchemeWithoutSchemeThrows(OpenApiSpecVersion version) { // Arrange @@ -258,7 +254,6 @@ public async Task SerializeHttpSecuritySchemeWithoutSchemeAsV2DoesNotThrow() [InlineData(OpenApiSpecVersion.OpenApi2_0)] [InlineData(OpenApiSpecVersion.OpenApi3_0)] [InlineData(OpenApiSpecVersion.OpenApi3_1)] - [InlineData(OpenApiSpecVersion.OpenApi3_2)] public Task SerializeOAuth2SecuritySchemeWithoutFlowsThrows(OpenApiSpecVersion version) { // Arrange @@ -277,7 +272,6 @@ public Task SerializeOAuth2SecuritySchemeWithoutFlowsThrows(OpenApiSpecVersion v [Theory] [InlineData(OpenApiSpecVersion.OpenApi3_0)] [InlineData(OpenApiSpecVersion.OpenApi3_1)] - [InlineData(OpenApiSpecVersion.OpenApi3_2)] public Task SerializeOpenIdConnectSecuritySchemeWithoutOpenIdConnectUrlThrows(OpenApiSpecVersion version) { // Arrange