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..8c7300aba 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,149 @@ public async Task SerializeApiKeySecuritySchemeAsV3YamlWorks() Assert.Equal(expected, actual); } + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + 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)] + 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)] + 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)] + 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)] + 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)] + 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 +492,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); + } } }