diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index 7c9e2dd37..f83636ca9 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -183,7 +183,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version // tags writer.WriteOptionalCollection( OpenApiConstants.Tags, - VerifyTagReferences(Tags), + Tags, callback); // summary @@ -237,7 +237,7 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) // tags writer.WriteOptionalCollection( OpenApiConstants.Tags, - VerifyTagReferences(Tags), + Tags, (w, t) => t.SerializeAsV2(w)); // summary @@ -356,21 +356,5 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) writer.WriteEndObject(); } - - private static HashSet? VerifyTagReferences(HashSet? tags) - { - if (tags?.Count > 0) - { - foreach (var tag in tags) - { - if (tag.Target is null) - { - throw new OpenApiException($"The OpenAPI tag reference '{tag.Reference.Id}' does not reference a valid tag."); - } - } - } - - return tags; - } } } diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs index 769764c9d..6f46a3fe7 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs @@ -59,7 +59,7 @@ public string? Description public Dictionary? Extensions { get => Target?.Extensions; } /// - public string? Name { get => Target?.Name; } + public string? Name { get => Target?.Name ?? Reference?.Id; } /// public override IOpenApiTag CopyReferenceAsTargetElementWithOverrides(IOpenApiTag source) { diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs index 351365065..21e254115 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs @@ -860,34 +860,5 @@ public void OpenApiOperationCopyConstructorWithAnnotationsSucceeds() Assert.NotEqual(baseOperation.Metadata["key1"], actualOperation.Metadata["key1"]); } - - [Theory] - [InlineData(OpenApiSpecVersion.OpenApi2_0)] - [InlineData(OpenApiSpecVersion.OpenApi3_0)] - [InlineData(OpenApiSpecVersion.OpenApi3_1)] - public async Task SerializeAsJsonAsyncThrowsIfTagReferenceIsUnresolved(OpenApiSpecVersion version) - { - var document = new OpenApiDocument() - { - Tags = - [ - new() { Name = "one" }, - new() { Name = "three" } - ] - }; - - var operation = new OpenApiOperation() - { - Tags = - [ - new OpenApiTagReference("one", document), - new OpenApiTagReference("two", document), - new OpenApiTagReference("three", document) - ] - }; - - var exception = await Assert.ThrowsAsync(() => operation.SerializeAsJsonAsync(version)); - Assert.Equal("The OpenAPI tag reference 'two' does not reference a valid tag.", exception.Message); - } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiTagReferenceTest.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiTagReferenceTest.cs index 82bd277d7..bdad6ff93 100644 --- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiTagReferenceTest.cs +++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiTagReferenceTest.cs @@ -8,18 +8,21 @@ using System.Threading.Tasks; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models.References; -using Microsoft.OpenApi.Reader; -using Microsoft.OpenApi.YamlReader; using Microsoft.OpenApi.Writers; using VerifyXunit; using Xunit; +using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Linq; -namespace Microsoft.OpenApi.Tests.Models.References +namespace Microsoft.OpenApi.Tests.Models.References; + +[Collection("DefaultSettings")] +public class OpenApiTagReferenceTest { - [Collection("DefaultSettings")] - public class OpenApiTagReferenceTest - { - private const string OpenApi = @"openapi: 3.0.3 + private const string OpenApi = +""" +openapi: 3.0.3 info: title: Sample API version: 1.0.0 @@ -59,97 +62,267 @@ public class OpenApiTagReferenceTest tags: - name: user description: Operations about users. -"; +"""; - readonly OpenApiTagReference _openApiTagReference; - readonly OpenApiTagReference _openApiTagReference2; - readonly OpenApiDocument _openApiDocument; + readonly OpenApiTagReference _openApiTagReference; + readonly OpenApiTagReference _openApiTagReference2; + readonly OpenApiDocument _openApiDocument; - public OpenApiTagReferenceTest() - { - var result = OpenApiDocument.Parse(OpenApi, "yaml", SettingsFixture.ReaderSettings); - _openApiDocument = result.Document; - _openApiTagReference = new("user", result.Document); - _openApiTagReference2 = new("users.user", result.Document); - } + public OpenApiTagReferenceTest() + { + var result = OpenApiDocument.Parse(OpenApi, "yaml", SettingsFixture.ReaderSettings); + _openApiDocument = result.Document; + _openApiTagReference = new("user", result.Document); + _openApiTagReference2 = new("users.user", result.Document); + } - [Fact] - public void TagReferenceResolutionWorks() - { - // Assert - Assert.Equal("user", _openApiTagReference.Name); - Assert.Equal("Operations about users.", _openApiTagReference.Description); - Assert.True(_openApiTagReference2.UnresolvedReference);// the target is null - var operationTags = _openApiDocument.Paths["/users/{userId}"].Operations[HttpMethod.Get].Tags; - Assert.Null(operationTags); // the operation tags are not loaded due to the invalid syntax at the operation level(should be a list of strings) - } + [Fact] + public void TagReferenceResolutionWorks() + { + // Assert + Assert.Equal("user", _openApiTagReference.Name); + Assert.Equal("Operations about users.", _openApiTagReference.Description); + Assert.True(_openApiTagReference2.UnresolvedReference);// the target is null + var operationTags = _openApiDocument.Paths["/users/{userId}"].Operations[HttpMethod.Get].Tags; + Assert.Null(operationTags); // the operation tags are not loaded due to the invalid syntax at the operation level(should be a list of strings) + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SerializeTagReferenceAsV3JsonWorks(bool produceTerseOutput) - { - // Arrange - var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); - var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeTagReferenceAsV3JsonWorks(bool produceTerseOutput) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); - // Act - _openApiTagReference.SerializeAsV3(writer); - await writer.FlushAsync(); + // Act + _openApiTagReference.SerializeAsV3(writer); + await writer.FlushAsync(); - // Assert - await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); - } + // Assert + await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SerializeTagReferenceAsV31JsonWorks(bool produceTerseOutput) - { - // Arrange - var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); - var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeTagReferenceAsV31JsonWorks(bool produceTerseOutput) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); - // Act - _openApiTagReference.SerializeAsV31(writer); - await writer.FlushAsync(); + // Act + _openApiTagReference.SerializeAsV31(writer); + await writer.FlushAsync(); - // Assert - await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SerializeTagAsV3JsonWorks(bool produceTerseOutput) - { - // Arrange - var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); - var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); + // Assert + await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeTagAsV3JsonWorks(bool produceTerseOutput) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); - // Act - _openApiTagReference2.SerializeAsV3(writer); - await writer.FlushAsync(); + // Act + _openApiTagReference2.SerializeAsV3(writer); + await writer.FlushAsync(); - // Assert - Assert.Equal("\"users.user\"", outputStringWriter.ToString()); + // Assert + Assert.Equal("\"users.user\"", outputStringWriter.ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeTagAsV31JsonWorks(bool produceTerseOutput) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); + + // Act + _openApiTagReference2.SerializeAsV31(writer); + await writer.FlushAsync(); + + // Assert + Assert.Equal("\"users.user\"", outputStringWriter.ToString()); + } + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + public async Task SerializesADescriptionWithNoMatchingTagDefinition(OpenApiSpecVersion version) + { + var document = new OpenApiDocument + { + Paths = new OpenApiPaths() + { + ["/test"] = new OpenApiPathItem + { + Operations = new() + { + [HttpMethod.Get] = new OpenApiOperation + { + Tags = new HashSet + { + new OpenApiTagReference("user") + } + } + } + } + }, + }; + + // When + //serialize the document as json + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = true }); + switch (version) + { + case OpenApiSpecVersion.OpenApi2_0: + document.SerializeAsV2(writer); + break; + case OpenApiSpecVersion.OpenApi3_0: + document.SerializeAsV3(writer); + break; + case OpenApiSpecVersion.OpenApi3_1: + document.SerializeAsV31(writer); + break; } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SerializeTagAsV31JsonWorks(bool produceTerseOutput) + await writer.FlushAsync(); + + var expected = version switch { - // Arrange - var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); - var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); + OpenApiSpecVersion.OpenApi2_0 => +""" +{ + "swagger": "2.0", + "info": {}, + "paths": { + "/test": { + "get": { + "tags": [ + "user" + ], + "responses": {} + } + } + } +} +""", + OpenApiSpecVersion.OpenApi3_0 => +""" +{ + "openapi": "3.0.4", + "info": {}, + "paths": { + "/test": { + "get": { + "tags": [ + "user" + ], + "responses": {} + } + } + } +} +""", + OpenApiSpecVersion.OpenApi3_1 => +""" +{ + "openapi": "3.1.1", + "info": {}, + "paths": { + "/test": { + "get": { + "tags": [ + "user" + ], + "responses": {} + } + } + } +} +""", + _ => throw new ArgumentOutOfRangeException(nameof(version), version, null) + }; - // Act - _openApiTagReference2.SerializeAsV31(writer); - await writer.FlushAsync(); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(outputStringWriter.ToString()))); + } + [Theory] + [InlineData( +""" +{ + "openapi": "3.0.4", + "info": {}, + "paths": { + "/test": { + "get": { + "tags": [ + "user" + ], + "responses": {} + } + } + } +} +""" + )] + [InlineData( +""" +{ + "openapi": "3.1.1", + "info": {}, + "paths": { + "/test": { + "get": { + "tags": [ + "user" + ], + "responses": {} + } + } + } +} +""" + )] + [InlineData( +""" +{ + "swagger": "2.0", + "info": {}, + "paths": { + "/test": { + "get": { + "tags": [ + "user" + ], + "responses": {} + } + } + } +} +""" + )] + public void DeserializesADescriptionWithNoMatchingTagDefinition(string description) + { + // Given + var (document, _) = OpenApiDocument.Parse(description, "json", SettingsFixture.ReaderSettings); + + // When + var tagReference = document.Paths["/test"].Operations[HttpMethod.Get].Tags; + + // Then + Assert.NotNull(tagReference); + Assert.Single(tagReference); + Assert.Contains("user", tagReference.Select(static t => t.Name), StringComparer.Ordinal); - // Assert - Assert.Equal("\"users.user\"", outputStringWriter.ToString()); - } } }