diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Exceptions/InvalidOperationIdException.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Exceptions/InvalidOperationIdException.cs new file mode 100644 index 0000000..c48d7f0 --- /dev/null +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Exceptions/InvalidOperationIdException.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; + +namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Exceptions +{ + /// + /// The exception that is recorded when it is expected to have operationId XML tag + /// but it is missing or there are more than one tags. + /// + [Serializable] + internal class InvalidOperationIdException : DocumentationException + { + /// + /// Initializes a new instance of the . + /// + /// Error message. + public InvalidOperationIdException(string message = "") + : base(message) + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Extensions/StringExtensions.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Extensions/StringExtensions.cs index 6a43501..e5196de 100644 --- a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Extensions/StringExtensions.cs +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Extensions/StringExtensions.cs @@ -245,15 +245,5 @@ public static string ToTitleCase(this string value) return value.Substring(startIndex: 0, length: 1).ToUpperInvariant() + value.Substring(startIndex: 1); } - - /// - /// Extracts the absolute path from a full URL string. - /// - /// The string in URL format. - /// The absolute path inside the URL. - public static string UrlStringToAbsolutePath(this string value) - { - return WebUtility.UrlDecode(new Uri(value).AbsolutePath); - } } } \ No newline at end of file diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/FilterSet.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/FilterSet.cs index 968e15f..1989ce8 100644 --- a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/FilterSet.cs +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/FilterSet.cs @@ -113,7 +113,7 @@ public static FilterSet GetDefaultFilterSet(FilterSetVersion version) _defaultFilterSet.Add(new PopulateInAttributeFilter()); _defaultFilterSet.Add(new ConvertAlternativeParamTagsFilter()); _defaultFilterSet.Add(new ValidateInAttributeFilter()); - _defaultFilterSet.Add(new BranchOptionalPathParametersFilter()); + _defaultFilterSet.Add(new CreateOperationMetaFilter()); //Post processing document filters _defaultFilterSet.Add(new RemoveFailedGenerationOperationFilter()); diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/InternalOpenApiGenerator.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/InternalOpenApiGenerator.cs index da7d3d9..a9b3ed7 100644 --- a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/InternalOpenApiGenerator.cs +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/InternalOpenApiGenerator.cs @@ -501,7 +501,7 @@ private IList GenerateSpecificationDocuments( try { - operationMethod = OperationHandler.GetOperationMethod(url, operationElement); + operationMethod = OperationHandler.GetOperationMethod(operationElement); } catch (InvalidVerbException e) { diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Models/KnownStrings/KnownXmlStrings.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Models/KnownStrings/KnownXmlStrings.cs index 5f6d84e..903f87e 100644 --- a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Models/KnownStrings/KnownXmlStrings.cs +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/Models/KnownStrings/KnownXmlStrings.cs @@ -64,6 +64,7 @@ public class KnownXmlStrings public const string AuthorizationCode = "authorizationCode"; public const string ClientCredentials = "clientCredentials"; public const string Scope = "scope"; + public const string OperationId = "operationId"; public static string[] AllowedAppKeyInValues => new[] {Header, Query, Cookie}; public static string[] AllowedFlowTypeValues => new[] diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/OperationHandler.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/OperationHandler.cs index 329eab2..8e24f93 100644 --- a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/OperationHandler.cs +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/OperationHandler.cs @@ -19,9 +19,40 @@ namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration internal static class OperationHandler { /// - /// Gets the operation id by parsing segments out of the absolute path. + /// Checks whether the optional operation id tag is present in the operation element. /// - public static string GetOperationId(string absolutePath, OperationType operationMethod) + /// + /// True if the operationId tag is in the operation element, otherwise false. + public static bool HasOperationId(XElement operationElement) + { + return operationElement?.Elements().Where(p => p.Name == KnownXmlStrings.OperationId).Any() ?? false; + } + + /// + /// Extracts the operation id from the operation element. + /// + /// + /// Operation id. + /// Thrown if the operationId tag is missing or + /// there are more than one tags. + public static string GetOperationId(XElement operationElement) + { + var operationIdList = operationElement?.Elements().Where(p => p.Name == KnownXmlStrings.OperationId).ToList(); + if (operationIdList?.Count == 1) + { + return operationIdList[0].Value; + } + + string error = operationIdList.Count > 1 + ? SpecificationGenerationMessages.MultipleOperationId + : SpecificationGenerationMessages.NoOperationId; + throw new InvalidOperationIdException(error); + } + + /// + /// Generates the operation id by parsing segments out of the absolute path. + /// + public static string GenerateOperationId(string absolutePath, OperationType operationMethod) { var operationId = new StringBuilder(operationMethod.ToString().ToLowerInvariant()); @@ -63,10 +94,9 @@ public static string GetOperationId(string absolutePath, OperationType operation /// /// Extracts the operation method from the operation element /// + /// The xml element representing an operation in the annotation xml. /// Thrown if the verb is missing or has invalid format. - public static OperationType GetOperationMethod( - string url, - XElement operationElement) + public static OperationType GetOperationMethod(XElement operationElement) { var verbElement = operationElement.Descendants().FirstOrDefault(i => i.Name == KnownXmlStrings.Verb); @@ -85,6 +115,7 @@ public static OperationType GetOperationMethod( /// /// Extracts the URL from the operation element /// + /// The xml element representing an operation in the annotation xml. /// Thrown if the URL is missing or has invalid format. public static string GetUrl( XElement operationElement) @@ -103,7 +134,7 @@ public static string GetUrl( try { - url = WebUtility.UrlDecode(new Uri(url).AbsolutePath); + url = WebUtility.UrlDecode(new Uri(WebUtility.UrlDecode(url)).AbsolutePath); } catch (UriFormatException) { diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/BranchOptionalPathParametersFilter.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/BranchOptionalPathParametersFilter.cs index ad82087..ad40a52 100644 --- a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/BranchOptionalPathParametersFilter.cs +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/BranchOptionalPathParametersFilter.cs @@ -19,8 +19,18 @@ namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.PreprocessingOp /// Parses the value of the URL and creates multiple operations in the Paths object when /// there are optional path parameters. /// - public class BranchOptionalPathParametersFilter : IPreProcessingOperationFilter + public class BranchOptionalPathParametersFilter : ICreateOperationPreProcessingOperationFilter { + /// + /// Verifies that the annotation XML element contains all data which are required to apply this filter. + /// + /// The xml element representing an operation in the annotation xml. + /// Always true (this filter can be always applied). + public bool IsApplicable(XElement element) + { + return true; + } + /// /// Fetches the URL value and creates multiple operations based on optional parameters. /// @@ -37,30 +47,18 @@ public IList Apply( try { - var paramElements = element.Elements() + var paramPathElements = element.Elements() .Where( p => p.Name == KnownXmlStrings.Param) + .Where( + p => p.Attribute(KnownXmlStrings.In)?.Value == KnownXmlStrings.Path) .ToList(); - // We need both the full URL and the absolute paths for processing. - // Full URL contains all path and query parameters. - // Absolute path is needed to get OperationId parsed out correctly. - var fullUrl = element.Elements() - .FirstOrDefault(p => p.Name == KnownXmlStrings.Url) - ?.Value; - - var absolutePath = fullUrl.UrlStringToAbsolutePath(); + var absolutePath = OperationHandler.GetUrl(element); - var operationMethod = (OperationType)Enum.Parse( - typeof(OperationType), - element.Elements().FirstOrDefault(p => p.Name == KnownXmlStrings.Verb)?.Value, - ignoreCase: true); + var allGeneratedPathStrings = GeneratePossiblePaths(absolutePath, paramPathElements); - var allGeneratedPathStrings = GeneratePossiblePaths( - absolutePath, - paramElements.Where( - p => p.Attribute(KnownXmlStrings.In)?.Value == KnownXmlStrings.Path) - .ToList()); + var operationMethod = OperationHandler.GetOperationMethod(element); foreach (var pathString in allGeneratedPathStrings) { @@ -72,7 +70,7 @@ public IList Apply( paths[pathString].Operations[operationMethod] = new OpenApiOperation { - OperationId = OperationHandler.GetOperationId(pathString, operationMethod) + OperationId = OperationHandler.GenerateOperationId(pathString, operationMethod) }; } } diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/CreateOperationMetaFilter.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/CreateOperationMetaFilter.cs new file mode 100644 index 0000000..3ea090b --- /dev/null +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/CreateOperationMetaFilter.cs @@ -0,0 +1,81 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Models; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.PreprocessingOperationFilters +{ + /// + /// Filter to initialize OpenApi operation based on the annotation XML element. + /// It does not do the creation itself but forwards the call to the real generator filters. + /// The first filter which is applicable is executed in order to generate the operation. + /// + public class CreateOperationMetaFilter : IPreProcessingOperationFilter + { + private List createOperationFilters; + + /// + /// Initializes a new instance of the . + /// + /// + /// Using this constructor, the following filter list is used: + /// If the XML element contains 'operationId' tag, it is used as unique identifier + /// of the operation. Otherwise, the id is generated using the path of the operation. + /// In the latter case, multiple operation could be generated, if the path has optional + /// parameters. + /// + public CreateOperationMetaFilter() : + this(new List { + new UsePredefinedOperationIdFilter(), new BranchOptionalPathParametersFilter()}) + { + } + + /// + /// Initializes a new instance of the . + /// + /// List of generator filters. + internal CreateOperationMetaFilter( + List createOperationFilters) + { + this.createOperationFilters = createOperationFilters; + } + + /// + /// Initializes the OpenApi operation based on the annotation XML element. + /// It applies the first applicable filter to create operations. + /// + /// The paths to be updated. + /// The xml element representing an operation in the annotation xml. + /// The operation filter settings. + /// The list of generation errors, if any produced when processing the filter. + public IList Apply( + OpenApiPaths paths, + XElement element, + PreProcessingOperationFilterSettings settings) + { + foreach (var filter in this.createOperationFilters) + { + if (filter.IsApplicable(element)) + { + return filter.Apply(paths, element, settings); + } + } + + // If none of the filters could be applied --> error + return new List + { + new GenerationError + { + Message = "Failed to apply any operation creation filter.", + ExceptionType = nameof(InvalidOperationException) + } + }; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/ICreateOperationPreProcessingOperationFilter.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/ICreateOperationPreProcessingOperationFilter.cs new file mode 100644 index 0000000..3e84540 --- /dev/null +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/ICreateOperationPreProcessingOperationFilter.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System.Collections.Generic; +using System.Xml.Linq; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Models; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.OperationFilters; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.PreprocessingOperationFilters +{ + /// + /// The class representing the contract of a filter to preprocess the + /// objects in before each is processed by the + /// . + /// + public interface ICreateOperationPreProcessingOperationFilter : IPreProcessingOperationFilter + { + /// + /// Verifies that the annotation XML element contains all data which are required to apply this filter. + /// + /// The xml element representing an operation in the annotation xml. + /// True if the filter can be applied, otherwise false. + bool IsApplicable(XElement element); + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/UsePredefinedOperationIdFilter.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/UsePredefinedOperationIdFilter.cs new file mode 100644 index 0000000..48b36ce --- /dev/null +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/PreprocessingOperationFilters/UsePredefinedOperationIdFilter.cs @@ -0,0 +1,111 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Models; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.PreprocessingOperationFilters +{ + /// + /// Filter to creates operation using the 'operationId' tag of the operation. + /// It does not consider optional parameters, it create one operation for the full path. + /// + public class UsePredefinedOperationIdFilter : ICreateOperationPreProcessingOperationFilter + { + private readonly Func GetUrlFunc; + private readonly Func GetOperationMethodFunc; + private readonly Func GetOperationIdFunc; + private readonly Predicate HasOperationIdFunc; + + /// + /// Initializes a new production instance of the . + /// + public UsePredefinedOperationIdFilter() + : this( + OperationHandler.GetUrl, + OperationHandler.GetOperationMethod, + OperationHandler.GetOperationId, + OperationHandler.HasOperationId) + { + } + + /// + /// Initializes a new instance of the . + /// + /// Get url function. + /// Get operation method function. + /// Get operation id function. + /// Has operation id function. + internal UsePredefinedOperationIdFilter( + Func GetUrlFunc, + Func GetOperationMethodFunc, + Func GetOperationIdFunc, + Predicate HasOperationIdFunc) + { + this.GetUrlFunc = GetUrlFunc; + this.GetOperationMethodFunc = GetOperationMethodFunc; + this.GetOperationIdFunc = GetOperationIdFunc; + this.HasOperationIdFunc = HasOperationIdFunc; + } + + /// + /// Verifies whether the annotation XML element contains operationId XML tag. + /// + /// The xml element representing an operation in the annotation xml. + /// + public bool IsApplicable(XElement element) + { + return this.HasOperationIdFunc(element); + } + + /// + /// Creates the operation using the operationId tag of the operation. It does not consider optional + /// parameters, it creates one operation for the full path. + /// + /// The paths to be updated. + /// The xml element representing an operation in the annotation xml. + /// The operation filter settings. + /// The list of generation errors, if any produced when processing the filter. + public IList Apply( + OpenApiPaths paths, + XElement element, + PreProcessingOperationFilterSettings settings) + { + var generationErrors = new List(); + + try + { + var absolutePath = this.GetUrlFunc(element); + var operationMethod = this.GetOperationMethodFunc(element); + string operationId = this.GetOperationIdFunc(element); + + if (!paths.ContainsKey(absolutePath)) + { + paths[absolutePath] = new OpenApiPathItem(); + } + + paths[absolutePath].Operations[operationMethod] = + new OpenApiOperation + { + OperationId = operationId + }; + } + catch(Exception ex) + { + generationErrors.Add( + new GenerationError + { + Message = ex.Message, + ExceptionType = ex.GetType().Name + }); + } + + return generationErrors; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/SpecificationGenerationMessages.cs b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/SpecificationGenerationMessages.cs index d83b683..328ab8d 100644 --- a/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/SpecificationGenerationMessages.cs +++ b/src/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration/SpecificationGenerationMessages.cs @@ -123,5 +123,9 @@ public static class SpecificationGenerationMessages public const string NotSupportedTypeAttributeValue = "Documented type: \"{0}\" is not supported for \"type\" attribute value in tag: \"{1}\". Supported values are: {2}."; + + public const string NoOperationId = "No operationId element is present in the operation."; + + public const string MultipleOperationId = "More than one operationId element is not accepted in the operation."; } } \ No newline at end of file diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/FilterTests/CreateOperationMetaFilterTest.cs b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/FilterTests/CreateOperationMetaFilterTest.cs new file mode 100644 index 0000000..25ca7be --- /dev/null +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/FilterTests/CreateOperationMetaFilterTest.cs @@ -0,0 +1,239 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using FluentAssertions; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Models; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.PreprocessingOperationFilters; +using Microsoft.OpenApi.Models; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.FilterTests +{ + [Collection("DefaultSettings")] + public class CreateOperationMetaFilterTest + { + private readonly ITestOutputHelper _output; + + public CreateOperationMetaFilterTest(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [MemberData(nameof(GetTestCasesForApplyShouldCallOnlyTheFirstFilterWhichIsApplicable))] + public void ApplyShouldCallOnlyTheFirstFilterWhichIsApplicable( + string testName, + List> mockFilters, + int firstApplicableFilterIndex) + { + _output.WriteLine(testName); + + // Prepare + var openApiPaths = new OpenApiPaths(); + XElement element = new XElement("elem"); + var settings = new PreProcessingOperationFilterSettings(); + + List filters = + mockFilters.Select(mockFilter => mockFilter.Object).ToList(); + + var filter = new CreateOperationMetaFilter(filters); + + // Action + filter.Apply(openApiPaths, element, settings); + + // Assert + for(int i = 0; i < firstApplicableFilterIndex - 1; ++i) + { + mockFilters[i].Verify(mock => mock.Apply( + It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never()); + } + + for (int i = firstApplicableFilterIndex + 1; i < mockFilters.Count; ++i) + { + mockFilters[i].Verify(mock => mock.Apply( + It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never()); + } + + mockFilters[firstApplicableFilterIndex].Verify(mock => mock.Apply( + openApiPaths, element, settings), Times.Once()); + } + + [Fact] + public void ApplyShouldReturnErrorIfNoneOfTheFiltersCouldBeApplied() + { + // Prepare + var openApiPaths = new OpenApiPaths(); + XElement element = new XElement("elem"); + var settings = new PreProcessingOperationFilterSettings(); + + var mockFilters = new List>{ + CreateMockFilter(false), + CreateMockFilter(false), + CreateMockFilter(false)}; + + List filters = + mockFilters.Select(mockFilter => mockFilter.Object).ToList(); + + var filter = new CreateOperationMetaFilter(filters); + + // Action + var errorList = filter.Apply(openApiPaths, element, settings); + + // Assert + errorList.Should().OnlyContain(error => error.ExceptionType == "InvalidOperationException"); + } + + [Fact] + public void ApplyShouldNotReturnErrorIfTheAppliedFilterDoesNotReturnError() + { + // Prepare + var openApiPaths = new OpenApiPaths(); + XElement element = new XElement("elem"); + var settings = new PreProcessingOperationFilterSettings(); + + var successfullFilter = CreateMockFilter(true); + + var mockFilters = new List>{ + successfullFilter}; + + List filters = + mockFilters.Select(mockFilter => mockFilter.Object).ToList(); + + var filter = new CreateOperationMetaFilter(filters); + + // Action + var errorList = filter.Apply(openApiPaths, element, settings); + + // Assert + errorList.Should().BeEmpty(); + } + + [Fact] + public void ApplyShouldReturnTheErrorListOfTheAppliedFilter() + { + // Prepare + var openApiPaths = new OpenApiPaths(); + XElement element = new XElement("elem"); + var settings = new PreProcessingOperationFilterSettings(); + + var mockFilterWithError = new Mock(); + + var expectedErrorList = new List + { + new GenerationError + { + Message = "Message_1", + ExceptionType = "ExceptionType_1" + }, + new GenerationError + { + Message = "Message_2", + ExceptionType = "ExceptionType_2" + } + }; + + mockFilterWithError.Setup( + mock => mock.Apply( + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(expectedErrorList); + + mockFilterWithError.Setup(mock => mock.IsApplicable(It.IsAny())).Returns(true); + + var mockFilters = new List> + { + mockFilterWithError + }; + + List filters = + mockFilters.Select(mockFilter => mockFilter.Object).ToList(); + + // Action + var filter = new CreateOperationMetaFilter(filters); + var errorList = filter.Apply(openApiPaths, element, settings); + + // Assert + errorList.Should().ContainInOrder(expectedErrorList); + } + + public static IEnumerable GetTestCasesForApplyShouldCallOnlyTheFirstFilterWhichIsApplicable() + { + yield return new object[] + { + "Only the first filter is applicable", + new List>{ + CreateMockFilter(true), + CreateMockFilter(false), + CreateMockFilter(false) + }, + 0 + }; + + yield return new object[] +{ + "Only the last filter is applicable", + new List>{ + CreateMockFilter(false), + CreateMockFilter(false), + CreateMockFilter(true) + }, + 2 + }; + + yield return new object[] + { + "Only one filter in the middle is applicable", + new List>{ + CreateMockFilter(false), + CreateMockFilter(true), + CreateMockFilter(false) + }, + 1 + }; + + yield return new object[] + { + "The first applicable filter masks the other applicable filters", + new List>{ + CreateMockFilter(false), + CreateMockFilter(true), + CreateMockFilter(true) + }, + 1 + }; + + yield return new object[] + { + "If all filters are applicable, only the first one is called", + new List>{ + CreateMockFilter(true), + CreateMockFilter(true), + CreateMockFilter(true) + }, + 0 + }; + } + + private static Mock CreateMockFilter(bool applicable) + { + var mockFilter = new Mock(); + + mockFilter.Setup( + mock => mock.Apply( + It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new List()); + + mockFilter.Setup(mock => mock.IsApplicable(It.IsAny())).Returns(applicable); + + return mockFilter; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/FilterTests/UsePredefinedOperationIdFilterTest.cs b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/FilterTests/UsePredefinedOperationIdFilterTest.cs new file mode 100644 index 0000000..5f3faa3 --- /dev/null +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/FilterTests/UsePredefinedOperationIdFilterTest.cs @@ -0,0 +1,259 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using FluentAssertions; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Exceptions; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.PreprocessingOperationFilters; +using Microsoft.OpenApi.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.FilterTests +{ + [Collection("DefaultSettings")] + public class UsePredefinedOperationIdFilterTest + { + private readonly ITestOutputHelper _output; + + public UsePredefinedOperationIdFilterTest(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [MemberData(nameof(GetTestCasesForIsApplicableShouldReferToTheHasOperationIdPredicate))] + public void IsApplicableShouldReferToTheHasOperationIdPredicate( + string testName, + bool shouldBeApplicable) + { + _output.WriteLine(testName); + + // Prepare + XElement calledElement = null; + bool mockHasOperationIdFunc(XElement e) + { + calledElement = e; + return shouldBeApplicable; + } + + var filter = new UsePredefinedOperationIdFilter( + DefaultGetUrlFunc, + DefaultGetOperationMethodFunc, + DefaultGetOperationIdFunc, + mockHasOperationIdFunc); + + var element = XElement.Parse("Not used in this test due of the mocked function."); + + // Action + var result = filter.IsApplicable(element); + + // Assert + result.Should().Be(shouldBeApplicable); + } + + [Fact] + public void ApplyShouldCreateTheExpectedOpenApiPath() + { + // Prepare + var expectedUrl = "/expected/operation/path"; + var expectedOperationType = OperationType.Post; + var expectedOperationId = "Expected_Operation_Id"; + + string MockGetUrlFunc(XElement e) + { + return (string)expectedUrl.Clone(); + } + + OperationType MockGetOperationTypeFunc(XElement e) + { + return expectedOperationType; + } + + string MockGetOperationIdFunc(XElement e) + { + return (string)expectedOperationId.Clone(); + } + + + var filter = new UsePredefinedOperationIdFilter( + MockGetUrlFunc, + MockGetOperationTypeFunc, + MockGetOperationIdFunc, + DefaultHasOperationIdFunc); + + var element = XElement.Parse("Not used in this test due of the mocked functions."); + + // Action + var openApiPaths = new OpenApiPaths(); + var result = filter.Apply(openApiPaths, element, new PreProcessingOperationFilterSettings()); + + // Assert + result.Should().BeEmpty(); + + openApiPaths.Keys.Count.Should().Be(1); + openApiPaths.Should().ContainKey(expectedUrl); + + openApiPaths[expectedUrl].Operations.Keys.Count.Should().Be(1); + openApiPaths[expectedUrl].Operations.Should().ContainKey(expectedOperationType); + + openApiPaths[expectedUrl].Operations[expectedOperationType] + .OperationId.Should().BeEquivalentTo(expectedOperationId); + } + + [Fact] + public void ApplyShouldReturnTheExpectedErrorIfGetUrlThrowsException() + { + // Prepare + string MockGetUlrFunc(XElement e) + { + throw new InvalidUrlException(); + } + + var filter = new UsePredefinedOperationIdFilter( + MockGetUlrFunc, + DefaultGetOperationMethodFunc, + DefaultGetOperationIdFunc, + DefaultHasOperationIdFunc); + + var element = XElement.Parse("Not used in this test due of the mocked functions."); + + // Action + var openApiPaths = new OpenApiPaths(); + var result = filter.Apply(openApiPaths, element, new PreProcessingOperationFilterSettings()); + + // Assert + result.Count.Should().Be(1); + result[0].ExceptionType.Should().Be("InvalidUrlException"); + openApiPaths.Keys.Count.Should().Be(0); + } + + [Fact] + public void ApplyShouldReturnTheExpectedErrorIfGetOperationMethodThrowsException() + { + // Prepare + OperationType MockGetOperationMethodFunc(XElement e) + { + throw new InvalidVerbException(); + } + + var filter = new UsePredefinedOperationIdFilter( + DefaultGetUrlFunc, + MockGetOperationMethodFunc, + DefaultGetOperationIdFunc, + DefaultHasOperationIdFunc); + + var element = XElement.Parse("Not used in this test due of the mocked functions."); + + // Action + var openApiPaths = new OpenApiPaths(); + var result = filter.Apply(openApiPaths, element, new PreProcessingOperationFilterSettings()); + + // Assert + result.Count.Should().Be(1); + result[0].ExceptionType.Should().Be("InvalidVerbException"); + openApiPaths.Keys.Count.Should().Be(0); + } + + [Fact] + public void ApplyShouldReturnTheExpectedErrorIfGetOperationIdThrowsException() + { + // Prepare + string MockGetOperationIdFunc(XElement e) + { + throw new InvalidOperationIdException(); + } + + var filter = new UsePredefinedOperationIdFilter( + DefaultGetUrlFunc, + DefaultGetOperationMethodFunc, + MockGetOperationIdFunc, + DefaultHasOperationIdFunc); + + var element = XElement.Parse("Not used in this test due of the mocked functions."); + + // Action + var openApiPaths = new OpenApiPaths(); + var result = filter.Apply(openApiPaths, element, new PreProcessingOperationFilterSettings()); + + // Assert + result.Count.Should().Be(1); + result[0].ExceptionType.Should().Be("InvalidOperationIdException"); + openApiPaths.Keys.Count.Should().Be(0); + } + + public static IEnumerable GetTestCasesForIsApplicableShouldReferToTheHasOperationIdPredicate() + { + yield return new object[] + { + "Applicable is HasOperationId returns true", + true + }; + + yield return new object[] + { + "Not applicable is HasOperationId returns false", + false + }; + } + + [Fact] + public void ApplyShouldProcessTheXmlTagProperlyEndToEnd() + { + // Prepare + var element = XElement.Parse(@" + + Assign Role + Role + https://localhost/v2/role/{role}/assign + put + AccessControl_AssignRole + + "); + + var openApiPaths = new OpenApiPaths(); + var settings = new PreProcessingOperationFilterSettings(); + + var filter = new UsePredefinedOperationIdFilter(); + + // Action + filter.Apply(openApiPaths, element, settings); + + // Assert + string url = "/v2/role/{role}/assign"; + OperationType operationType = OperationType.Put; + var expectedOperationId = "AccessControl_AssignRole"; + + openApiPaths.Should().ContainKey(url); + openApiPaths[url].Operations.Should().ContainKey(operationType); + + openApiPaths[url].Operations[operationType] + .OperationId.Should().BeEquivalentTo(expectedOperationId); + } + + private static string DefaultGetUrlFunc(XElement e) + { + return "/default/url"; + } + + private static OperationType DefaultGetOperationMethodFunc(XElement e) + { + return OperationType.Post; + } + + private static string DefaultGetOperationIdFunc(XElement e) + { + return "Default_Operation_Id"; + } + + private static bool DefaultHasOperationIdFunc(XElement e) + { + return true; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.csproj b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.csproj index d8d66db..3cbc957 100644 --- a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.csproj +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.csproj @@ -20,6 +20,7 @@ + @@ -44,6 +45,18 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Input/AnnotationPredefinedAndGeneratedOperationId.xml b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Input/AnnotationPredefinedAndGeneratedOperationId.xml new file mode 100644 index 0000000..ba9a014 --- /dev/null +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Input/AnnotationPredefinedAndGeneratedOperationId.xml @@ -0,0 +1,249 @@ + + + + Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.SampleApis + + + + + Responsible for route configuration + + + + + Registers routes + + Route collection + + + + Web config. + + + + + Register the configuration, services, and routes. + + HTTP Configuration + + + + Define V1 operations. + + + + + Sample Get 1 + + Sample V1 + GET + SampleControllerV1_SampleGet1 + http://localhost:9000/V1/samples/{id}?queryBool={queryBool} + Header param 1 + + + + where T is + where T1 is + where T2 is Header param 2 + + + Header param 3 + + The object id + Sample query boolean + + > +
Test Header
+ > + Sample object retrieved +
+ + Bad request + + Group1 + Group2 + The sample object 1 +
+ + + Sample Get 2 + + Sample V1 + GET + http://localhost:9000/V1/samples + Header param 1 + Header param 2 + Header param 3 + + Paged Entity contract + + + Bad request + + The sample object 3 + + + + Sample Post + + Sample V1 + POST + http://localhost:9000/V1/samples + Header param 1 + Header param 2 + Header param 3 + + Sample object + + + Sample object posted + + + Bad request + + + + + Sample put + + Sample V1 + PUT + SampleControllerV1_SamplePut + http://localhost:9000/V1/samples/{id} + Header param 1 + Header param 2 + Header param 3 + The object id + + Sample object + + + The sample object updated + + + Bad request + + The sample object 1 + + + + Defines V3 operations. + + + + + Sample get 1 + + Sample V3 + GET + http://localhost:9000/V3/samples/ + Header param 1 + Header param 2 + Header param 3 + + + where T is + where T1 is + where T2 is + List of sample objects. + + + Bad request + + + + + Sample get 2 + + Sample V3 + GET + SampleControllerV3_SampleGet2 + http://localhost:9000/V3/samples/{id}?queryString={queryString} + Header param 1 + Header param 2 + Header param 3 + The object id + The sample query string + + + where T1 is + where T2 is + List of sample objects. + + + Bad request + + + + + Defines V2 operations. + + + + + Sample delete + + Sample V2 + DELETE + SampleControllerV2_DeleteEntity + http://localhost:9000/V2/samples/{id} + Header param 1 + Header param 2 + Header param 3 + The object id + + Sample object deleted + + + Bad request + + + + + Sample get 1 + + Sample V2 + GET + http://localhost:9000/V2/samples/ + Header param 1 + Header param 2 + Header param 3 + + where T is List of sample objects + + + Bad request + + + + + Sample get 2 + + Sample V2 + GET + http://localhost:9000/V2/samples/{id}?queryString={queryString} + Header param 1 + Header param 2 + Header param 3 + The object id + The sample query string + + Sample object retrieved + + + Bad request + + + + + Web API Application. + + + + + Start application. + + +
+
diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Input/AnnotationPredefinedOperationId.xml b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Input/AnnotationPredefinedOperationId.xml new file mode 100644 index 0000000..7de4410 --- /dev/null +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Input/AnnotationPredefinedOperationId.xml @@ -0,0 +1,254 @@ + + + + Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.SampleApis + + + + + Responsible for route configuration + + + + + Registers routes + + Route collection + + + + Web config. + + + + + Register the configuration, services, and routes. + + HTTP Configuration + + + + Define V1 operations. + + + + + Sample Get 1 + + Sample V1 + GET + SampleControllerV1_SampleGet1 + http://localhost:9000/V1/samples/{id}?queryBool={queryBool} + Header param 1 + + + + where T is + where T1 is + where T2 is Header param 2 + + + Header param 3 + + The object id + Sample query boolean + + > +
Test Header
+ > + Sample object retrieved +
+ + Bad request + + Group1 + Group2 + The sample object 1 +
+ + + Sample Get 2 + + Sample V1 + GET + SampleControllerV1_SampleGet2 + http://localhost:9000/V1/samples + Header param 1 + Header param 2 + Header param 3 + + Paged Entity contract + + + Bad request + + The sample object 3 + + + + Sample Post + + Sample V1 + POST + SampleControllerV1_SamplePost + http://localhost:9000/V1/samples + Header param 1 + Header param 2 + Header param 3 + + Sample object + + + Sample object posted + + + Bad request + + + + + Sample put + + Sample V1 + PUT + SampleControllerV1_SamplePut + http://localhost:9000/V1/samples/{id} + Header param 1 + Header param 2 + Header param 3 + The object id + + Sample object + + + The sample object updated + + + Bad request + + The sample object 1 + + + + Defines V3 operations. + + + + + Sample get 1 + + Sample V3 + GET + SampleControllerV3_SampleGet1 + http://localhost:9000/V3/samples/ + Header param 1 + Header param 2 + Header param 3 + + + where T is + where T1 is + where T2 is + List of sample objects. + + + Bad request + + + + + Sample get 2 + + Sample V3 + GET + SampleControllerV3_SampleGet2 + http://localhost:9000/V3/samples/{id}?queryString={queryString} + Header param 1 + Header param 2 + Header param 3 + The object id + The sample query string + + + where T1 is + where T2 is + List of sample objects. + + + Bad request + + + + + Defines V2 operations. + + + + + Sample delete + + Sample V2 + DELETE + SampleControllerV2_DeleteEntity + http://localhost:9000/V2/samples/{id} + Header param 1 + Header param 2 + Header param 3 + The object id + + Sample object deleted + + + Bad request + + + + + Sample get 1 + + Sample V2 + GET + SampleControllerV2_SampleGet1 + http://localhost:9000/V2/samples/ + Header param 1 + Header param 2 + Header param 3 + + where T is List of sample objects + + + Bad request + + + + + Sample get 2 + + Sample V2 + GET + SampleControllerV2_SampleGet2 + http://localhost:9000/V2/samples/{id}?queryString={queryString} + Header param 1 + Header param 2 + Header param 3 + The object id + The sample query string + + Sample object retrieved + + + Bad request + + + + + Web API Application. + + + + + Start application. + + +
+
diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/OpenApiDocumentGeneratorTest.cs b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/OpenApiDocumentGeneratorTest.cs index f352eaf..2fcebc8 100644 --- a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/OpenApiDocumentGeneratorTest.cs +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/OpenApiDocumentGeneratorTest.cs @@ -1094,6 +1094,56 @@ public static IEnumerable GetTestCasesForValidDocumentationShouldRetur OutputDirectory, "AnnotationWithRuntimeSerialization.Json") }; + + yield return new object[] + { + "All operations have predefined operation Id", + new List + { + Path.Combine(InputDirectory, "AnnotationPredefinedOperationId.xml"), + Path.Combine(InputDirectory, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.xml") + }, + new List + { + Path.Combine( + InputDirectory, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.SampleApis.dll"), + Path.Combine( + InputDirectory, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.dll") + }, + "1.0.0", + 9, + Path.Combine( + OutputDirectory, + "AnnotationPredefinedOperationId.Json") + }; + + yield return new object[] + { + "A few operations have predefined operation Id, the others are generated", + new List + { + Path.Combine(InputDirectory, "AnnotationPredefinedAndGeneratedOperationId.xml"), + Path.Combine(InputDirectory, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.xml") + }, + new List + { + Path.Combine( + InputDirectory, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.SampleApis.dll"), + Path.Combine( + InputDirectory, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.dll") + }, + "1.0.0", + 9, + Path.Combine( + OutputDirectory, + "AnnotationPredefinedAndGeneratedOperationId.Json") + }; } [Theory] diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Output/AnnotationPredefinedAndGeneratedOperationId.Json b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Output/AnnotationPredefinedAndGeneratedOperationId.Json new file mode 100644 index 0000000..5811f81 --- /dev/null +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Output/AnnotationPredefinedAndGeneratedOperationId.Json @@ -0,0 +1,758 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.SampleApis", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost:9000" + } + ], + "paths": { + "/V1/samples/{id}": { + "get": { + "tags": [ + "Sample V1" + ], + "summary": "Sample Get 1", + "operationId": "SampleControllerV1_SampleGet1", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.ISampleObject4_Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1-Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2_" + } + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "The object id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "queryBool", + "in": "query", + "description": "Sample query boolean", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Sample object retrieved", + "headers": { + "TestHeader": { + "description": "Test Header", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "put": { + "tags": [ + "Sample V1" + ], + "summary": "Sample put", + "operationId": "SampleControllerV1_SamplePut", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "The object id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Sample object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "The sample object updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/V1/samples": { + "get": { + "tags": [ + "Sample V1" + ], + "summary": "Sample Get 2", + "operationId": "getV1Samples", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Paged Entity contract", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject3" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "tags": [ + "Sample V1" + ], + "summary": "Sample Post", + "operationId": "postV1Samples", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Sample object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Sample object posted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject3" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/V3/samples/": { + "get": { + "tags": [ + "Sample V3" + ], + "summary": "Sample get 1", + "operationId": "getV3Samples", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of sample objects.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.ISampleObject4_Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1-Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2_" + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/V3/samples/{id}": { + "get": { + "tags": [ + "Sample V3" + ], + "summary": "Sample get 2", + "operationId": "SampleControllerV3_SampleGet2", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "The object id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "queryString", + "in": "query", + "description": "The sample query string", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of sample objects.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.ISampleObject4_Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1-Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2_" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/V2/samples/{id}": { + "delete": { + "tags": [ + "Sample V2" + ], + "summary": "Sample delete", + "operationId": "SampleControllerV2_DeleteEntity", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "The object id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Sample object deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "get": { + "tags": [ + "Sample V2" + ], + "summary": "Sample get 2", + "operationId": "getV2SamplesById", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "The object id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "queryString", + "in": "query", + "description": "The sample query string", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Sample object retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/V2/samples/": { + "get": { + "tags": [ + "Sample V2" + ], + "summary": "Sample get 1", + "operationId": "getV2Samples", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of sample objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2" + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1": { + "required": [ + "samplePropertyString3", + "samplePropertyString4", + "samplePropertyEnum" + ], + "type": "object", + "properties": { + "samplePropertyBool": { + "type": "boolean", + "description": "Gets or sets the sample property bool" + }, + "samplePropertyStringInt": { + "type": "integer", + "description": "Gets or sets the sample property int", + "format": "int32" + }, + "samplePropertyString1": { + "type": "string", + "description": "Gets or sets the sample property string 1" + }, + "samplePropertyString2": { + "type": "string", + "description": "Gets or sets the sample property string 2" + }, + "samplePropertyString3": { + "type": "string", + "description": "Gets or sets the sample property string 3" + }, + "samplePropertyString4": { + "type": "string", + "description": "Gets or sets the sample property string 4" + }, + "samplePropertyEnum": { + "enum": [ + "SampleEnumValue1", + "SampleEnumValue2" + ], + "type": "string", + "description": "Gets or sets the sample property enum" + } + } + }, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.ISampleObject4_Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1-Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2_": { + "required": [ + "samplePropertyTypeT1", + "samplePropertyTypeT2" + ], + "type": "object", + "properties": { + "samplePropertyTypeT1": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + }, + "samplePropertyTypeT2": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2" + } + } + }, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2": { + "required": [ + "samplePropertyInt", + "samplePropertyObjectDictionary", + "samplePropertyObjectList" + ], + "type": "object", + "properties": { + "samplePropertyEnum": { + "enum": [ + "SampleEnumValue1", + "SampleEnumValue2" + ], + "type": "string", + "description": "Gets or sets the sample property enum" + }, + "samplePropertyInt": { + "type": "string", + "description": "Gets or sets the sample property string 1" + }, + "samplePropertyObjectDictionary": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + }, + "description": "Gets the sample property object dictionary" + }, + "samplePropertyObjectList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + }, + "description": "Gets the sample property object list" + }, + "samplePropertyString1": { + "type": "string", + "description": "Gets or sets the sample property string 1" + } + } + }, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject3": { + "required": [ + "samplePropertyObject" + ], + "type": "object", + "properties": { + "samplePropertyObject": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2" + }, + "samplePropertyObjectList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + }, + "description": "Gets the sample property object list" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Output/AnnotationPredefinedOperationId.Json b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Output/AnnotationPredefinedOperationId.Json new file mode 100644 index 0000000..6f5444a --- /dev/null +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OpenApiDocumentGeneratorTests/Output/AnnotationPredefinedOperationId.Json @@ -0,0 +1,758 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.SampleApis", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost:9000" + } + ], + "paths": { + "/V1/samples/{id}": { + "get": { + "tags": [ + "Sample V1" + ], + "summary": "Sample Get 1", + "operationId": "SampleControllerV1_SampleGet1", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.ISampleObject4_Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1-Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2_" + } + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "The object id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "queryBool", + "in": "query", + "description": "Sample query boolean", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Sample object retrieved", + "headers": { + "TestHeader": { + "description": "Test Header", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "put": { + "tags": [ + "Sample V1" + ], + "summary": "Sample put", + "operationId": "SampleControllerV1_SamplePut", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "The object id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Sample object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "The sample object updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/V1/samples": { + "get": { + "tags": [ + "Sample V1" + ], + "summary": "Sample Get 2", + "operationId": "SampleControllerV1_SampleGet2", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Paged Entity contract", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject3" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "tags": [ + "Sample V1" + ], + "summary": "Sample Post", + "operationId": "SampleControllerV1_SamplePost", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Sample object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Sample object posted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject3" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/V3/samples/": { + "get": { + "tags": [ + "Sample V3" + ], + "summary": "Sample get 1", + "operationId": "SampleControllerV3_SampleGet1", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of sample objects.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.ISampleObject4_Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1-Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2_" + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/V3/samples/{id}": { + "get": { + "tags": [ + "Sample V3" + ], + "summary": "Sample get 2", + "operationId": "SampleControllerV3_SampleGet2", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "The object id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "queryString", + "in": "query", + "description": "The sample query string", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of sample objects.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.ISampleObject4_Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1-Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2_" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/V2/samples/{id}": { + "delete": { + "tags": [ + "Sample V2" + ], + "summary": "Sample delete", + "operationId": "SampleControllerV2_DeleteEntity", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "The object id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Sample object deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "get": { + "tags": [ + "Sample V2" + ], + "summary": "Sample get 2", + "operationId": "SampleControllerV2_SampleGet2", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "The object id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "queryString", + "in": "query", + "description": "The sample query string", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Sample object retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/V2/samples/": { + "get": { + "tags": [ + "Sample V2" + ], + "summary": "Sample get 1", + "operationId": "SampleControllerV2_SampleGet1", + "parameters": [ + { + "name": "sampleHeaderParam1", + "in": "header", + "description": "Header param 1", + "schema": { + "type": "number", + "format": "float" + } + }, + { + "name": "sampleHeaderParam2", + "in": "header", + "description": "Header param 2", + "schema": { + "type": "string" + } + }, + { + "name": "sampleHeaderParam3", + "in": "header", + "description": "Header param 3", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of sample objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2" + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1": { + "required": [ + "samplePropertyString3", + "samplePropertyString4", + "samplePropertyEnum" + ], + "type": "object", + "properties": { + "samplePropertyBool": { + "type": "boolean", + "description": "Gets or sets the sample property bool" + }, + "samplePropertyStringInt": { + "type": "integer", + "description": "Gets or sets the sample property int", + "format": "int32" + }, + "samplePropertyString1": { + "type": "string", + "description": "Gets or sets the sample property string 1" + }, + "samplePropertyString2": { + "type": "string", + "description": "Gets or sets the sample property string 2" + }, + "samplePropertyString3": { + "type": "string", + "description": "Gets or sets the sample property string 3" + }, + "samplePropertyString4": { + "type": "string", + "description": "Gets or sets the sample property string 4" + }, + "samplePropertyEnum": { + "enum": [ + "SampleEnumValue1", + "SampleEnumValue2" + ], + "type": "string", + "description": "Gets or sets the sample property enum" + } + } + }, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.ISampleObject4_Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1-Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2_": { + "required": [ + "samplePropertyTypeT1", + "samplePropertyTypeT2" + ], + "type": "object", + "properties": { + "samplePropertyTypeT1": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + }, + "samplePropertyTypeT2": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2" + } + } + }, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2": { + "required": [ + "samplePropertyInt", + "samplePropertyObjectDictionary", + "samplePropertyObjectList" + ], + "type": "object", + "properties": { + "samplePropertyEnum": { + "enum": [ + "SampleEnumValue1", + "SampleEnumValue2" + ], + "type": "string", + "description": "Gets or sets the sample property enum" + }, + "samplePropertyInt": { + "type": "string", + "description": "Gets or sets the sample property string 1" + }, + "samplePropertyObjectDictionary": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + }, + "description": "Gets the sample property object dictionary" + }, + "samplePropertyObjectList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + }, + "description": "Gets the sample property object list" + }, + "samplePropertyString1": { + "type": "string", + "description": "Gets or sets the sample property string 1" + } + } + }, + "Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject3": { + "required": [ + "samplePropertyObject" + ], + "type": "object", + "properties": { + "samplePropertyObject": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject2" + }, + "samplePropertyObjectList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.Contracts.SampleObject1" + }, + "description": "Gets the sample property object list" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OperationHandlerTests/OperationHandlerGetUrlTests.cs b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OperationHandlerTests/OperationHandlerGetUrlTests.cs new file mode 100644 index 0000000..6cd761f --- /dev/null +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OperationHandlerTests/OperationHandlerGetUrlTests.cs @@ -0,0 +1,113 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Net; +using System.Xml.Linq; +using FluentAssertions; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Exceptions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.FilterTests +{ + [Collection("DefaultSettings")] + public class OperationHandlerGetUrlTests + { + private readonly ITestOutputHelper _output; + + public OperationHandlerGetUrlTests(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [MemberData(nameof(GetTestCasesForGetUrlShouldReturnTheAbsolutePathUsingUrlXmlTag))] + public void GetUrlShouldReturnTheAbsolutePathUsingUrlXmlTag( + string testName, + XElement element, + string expectedUrl) + { + _output.WriteLine(testName); + + // Action + var url = OperationHandler.GetUrl(element); + + // Assert + url.Should().BeEquivalentTo(expectedUrl); + } + + [Fact] + public void GetUrlShouldThrowInvalidUrlExceptionByMissingUrlTag() + { + var element = XElement.Parse($@" + + + + "); + + // Action + Action action = () => { OperationHandler.GetUrl(element); }; + + // Assert + action.Should().ThrowExactly(); + } + + + [Fact] + public void GetUrlShouldThrowInvalidUrlExceptionByWrongUrl() + { + var element = XElement.Parse($@" + + This is not a valid url. + + "); + + // Action + Action action = () => { OperationHandler.GetUrl(element); }; + + // Assert + action.Should().ThrowExactly(); + } + + public static IEnumerable GetTestCasesForGetUrlShouldReturnTheAbsolutePathUsingUrlXmlTag() + { + yield return new object[] + { + "Simple url works", + XElement.Parse(@" + + https://localhost/v2/role/{role}/assign + + "), + "/v2/role/{role}/assign" + }; + + yield return new object[] + { + "Complex encoded url works", + XElement.Parse($@" + + {WebUtility.UrlEncode("https://localhost/v2/role/{role}/assign?p1=1&p2=2")} + + "), + "/v2/role/{role}/assign" + }; + + yield return new object[] + { + "The first url is considerd", + XElement.Parse(@" + + https://localhost/v2/role/{role}/assign + https://localhost/this/url/should/not/be/considered + + "), + "/v2/role/{role}/assign" + }; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OperationHandlerTests/OperationHandlerOperationIdTests.cs b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OperationHandlerTests/OperationHandlerOperationIdTests.cs new file mode 100644 index 0000000..ad30f57 --- /dev/null +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OperationHandlerTests/OperationHandlerOperationIdTests.cs @@ -0,0 +1,146 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using FluentAssertions; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Exceptions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.FilterTests +{ + [Collection("DefaultSettings")] + public class OperationHandlerOperationIdTests + { + private readonly ITestOutputHelper _output; + + public OperationHandlerOperationIdTests(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [MemberData(nameof(GetTestCasesForHasOperationIdShouldReferToTheOperationIdXmlTag))] + public void HasOperationIdShouldReferToTheOperationIdXmlTag( + string testName, + XElement element, + bool expectedResult) + { + _output.WriteLine(testName); + + // Action + var result = OperationHandler.HasOperationId(element); + + // Assert + result.Should().Be(expectedResult); + } + + [Fact] + public void HasOperationIdShouldReturnFalseByNullInput() + { + // Action + var result = OperationHandler.HasOperationId(null); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void GetOperationIdShouldUseTheOperationIdXmlTagAsOperationId() + { + // Prepare + var element = XElement.Parse(@" + + AccessControl_AssignRole + + "); + + // Action + var result = OperationHandler.GetOperationId(element); + + // Assert + var expectedOperationId = "AccessControl_AssignRole"; + result.Should().BeEquivalentTo(expectedOperationId); + } + + [Theory] + [MemberData(nameof(GetTestCasesForGetOperationIdShouldThrowInvalidOperationIdExceptionByWrongXml))] + public void GetOperationIdShouldThrowInvalidOperationIdExceptionByWrongXml( + string testName, + XElement element) + { + _output.WriteLine(testName); + + // Action + Action action = () => { OperationHandler.GetOperationId(element); }; + + // Assert + action.Should().ThrowExactly(); + } + + public static IEnumerable GetTestCasesForHasOperationIdShouldReferToTheOperationIdXmlTag() + { + yield return new object[] + { + "Return false if no operationId XML tag is present", + XElement.Parse(@" + + + + "), + false + }; + + yield return new object[] + { + "Return true if one operationId XML tag is present", + XElement.Parse(@" + + AccessControl_AssignRole + + "), + true + }; + + yield return new object[] + { + "Return true if multiple operationId XML tags are present", + XElement.Parse(@" + + AssignRole + AccessControl_AssignRole + + "), + true + }; + } + + public static IEnumerable GetTestCasesForGetOperationIdShouldThrowInvalidOperationIdExceptionByWrongXml() + { + yield return new object[] + { + "Missing operationId tag", + XElement.Parse(@" + + + + ") + }; + + yield return new object[] + { + "More than one operationId tag", + XElement.Parse(@" + + First operation id + Second operation id + + ") + }; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OperationHandlerTests/OperationHandlerOperationMethodTests.cs b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OperationHandlerTests/OperationHandlerOperationMethodTests.cs new file mode 100644 index 0000000..c471ecc --- /dev/null +++ b/test/Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests/OperationHandlerTests/OperationHandlerOperationMethodTests.cs @@ -0,0 +1,91 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using FluentAssertions; +using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Exceptions; +using Microsoft.OpenApi.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Tests.FilterTests +{ + [Collection("DefaultSettings")] + public class OperationHandlerOperationMethodTests + { + private readonly ITestOutputHelper _output; + + public OperationHandlerOperationMethodTests(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [MemberData(nameof(GetTestCasesForGetOperationMethodShouldUseTheVerbXmlTagAsOperationType))] + public void GetOperationMethodShouldUseTheVerbXmlTagAsOperationType( + string testName, + XElement element, + OperationType expectedOperationType) + { + _output.WriteLine(testName); + + // Action + var result = OperationHandler.GetOperationMethod(element); + + // Assert + result.Should().BeEquivalentTo(expectedOperationType); + } + + [Fact] + public void GetOperationMethodShouldThrowInvalidVerbExceptionByMissingVerbTag() + { + var element = XElement.Parse($@" + + + + "); + + // Action + Action action = () => { OperationHandler.GetOperationMethod(element); }; + + // Assert + action.Should().ThrowExactly(); + } + + public static IEnumerable GetTestCasesForGetOperationMethodShouldUseTheVerbXmlTagAsOperationType() + { + var operationTypeStringList = new string[] + { + "get", "put", "post", "delete", "options", "head", "patch", "trace" + }; + + var expectedOperationTypeList = new OperationType[] + { + OperationType.Get, OperationType.Put, OperationType.Post, OperationType.Delete, + OperationType.Options, OperationType.Head, OperationType.Patch, OperationType.Trace + }; + + var operationTypeList = operationTypeStringList.Zip(expectedOperationTypeList, (s, t) => new { Input = s, Exp = t }); + foreach (var item in operationTypeList) + { + var element = XElement.Parse($@" + + {item.Input} + + "); + + yield return new object[] + { + "Test for " + item.Input, + element, + item.Exp + }; + } + } + } +} \ No newline at end of file