diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 0ad6a7274..c371e9786 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -389,7 +389,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version { writer.WriteStartObject(); - if (version == OpenApiSpecVersion.OpenApi3_1) + if (version >= OpenApiSpecVersion.OpenApi3_1) { WriteJsonSchemaKeywords(writer); } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiCallbackReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiCallbackReferenceDeserializerTests.cs new file mode 100644 index 000000000..d73bfbd2c --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiCallbackReferenceDeserializerTests.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiCallbackReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializeCallbackReferenceAnnotations() + { + var json = + """ + { + "$ref": "#/components/callbacks/MyCallback" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.AddComponent("MyCallback", new OpenApiCallback + { + // Optionally add a PathItem or similar here if needed + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadCallback(parseNode, hostDocument); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.Equal("MyCallback", resultReference.Reference.Id); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs new file mode 100644 index 000000000..da82e35e7 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiComponentsTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Reader; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests +{ + public class OpenApiComponentsTests + { + [Theory] + [InlineData("./FirstLevel/SecondLevel/ThridLevel/File.json#/components/schemas/ExternalRelativePathModel", "ExternalRelativePathModel", "./FirstLevel/SecondLevel/ThridLevel/File.json")] + [InlineData("File.json#/components/schemas/ExternalSimpleRelativePathModel", "ExternalSimpleRelativePathModel", "File.json")] + [InlineData("A:\\Dir\\File.json#/components/schemas/ExternalAbsWindowsPathModel", "ExternalAbsWindowsPathModel", "A:\\Dir\\File.json")] + [InlineData("/Dir/File.json#/components/schemas/ExternalAbsUnixPathModel", "ExternalAbsUnixPathModel", "/Dir/File.json")] + [InlineData("https://host.lan:1234/path/to/file/resource.json#/components/schemas/ExternalHttpsModel", "ExternalHttpsModel", "https://host.lan:1234/path/to/file/resource.json")] + [InlineData("File.json", "File.json", null)] + public void ParseExternalSchemaReferenceShouldSucceed(string reference, string referenceId, string externalResource) + { + var input = $@"{{ + ""schemas"": {{ + ""Model"": {{ + ""$ref"": ""{reference.Replace("\\", "\\\\")}"" + }} + }} +}} +"; + var openApiDocument = new OpenApiDocument(); + + // Act + var components = OpenApiModelFactory.Parse(input, OpenApiSpecVersion.OpenApi3_2, openApiDocument, out _, "json"); + + // Assert + var schema = components.Schemas["Model"] as OpenApiSchemaReference; + var expected = new OpenApiSchemaReference(referenceId, openApiDocument, externalResource); + Assert.Equivalent(expected, schema); + } + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentSerializationTests .cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentSerializationTests .cs new file mode 100644 index 000000000..2888e39e2 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentSerializationTests .cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests +{ + public class OpenApiDocumentSerializationTests + { + private const string SampleFolderPath = "V32Tests/Samples/OpenApiDocument/"; + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + public async Task Serialize_DoesNotMutateDom(OpenApiSpecVersion version) + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "docWith32properties.json"); + var (doc, _) = await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings); + + // Act: Serialize using System.Text.Json + var options = new JsonSerializerOptions + { + Converters = + { + new HttpMethodOperationDictionaryConverter() + }, + }; + var originalSerialized = JsonSerializer.Serialize(doc, options); + Assert.NotNull(originalSerialized); // sanity check + + // Serialize using native OpenAPI writer + var jsonWriter = new StringWriter(); + var openApiWriter = new OpenApiJsonWriter(jsonWriter); + switch (version) + { + case OpenApiSpecVersion.OpenApi3_2: + doc.SerializeAsV32(openApiWriter); + break; + case OpenApiSpecVersion.OpenApi3_1: + doc.SerializeAsV31(openApiWriter); + break; + case OpenApiSpecVersion.OpenApi3_0: + doc.SerializeAsV3(openApiWriter); + break; + default: + doc.SerializeAsV2(openApiWriter); + break; + } + + // Serialize again with STJ after native writer serialization + var finalSerialized = JsonSerializer.Serialize(doc, options); + Assert.NotNull(finalSerialized); // sanity check + + // Assert: Ensure no mutation occurred in the DOM after native serialization + Assert.True(JsonNode.DeepEquals(originalSerialized, finalSerialized), "OpenAPI DOM was mutated by the native serializer."); + } + } + + public class HttpMethodOperationDictionaryConverter : JsonConverter> + { + public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var kvp in value) + { + writer.WritePropertyName(kvp.Key.Method.ToLowerInvariant()); + JsonSerializer.Serialize(writer, kvp.Value, options); + } + + writer.WriteEndObject(); + } + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.ParseDocumentWith32PropertiesWorks.verified.txt b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.ParseDocumentWith32PropertiesWorks.verified.txt new file mode 100644 index 000000000..a9a5d5e1b --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.ParseDocumentWith32PropertiesWorks.verified.txt @@ -0,0 +1,116 @@ +openapi: '3.2.0' +jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema +info: + title: Sample OpenAPI 3.2 API + description: A sample API demonstrating OpenAPI 3.2 features + license: + name: Apache 2.0 + identifier: Apache-2.0 + version: 2.0.0 + summary: Sample OpenAPI 3.2 API with the latest features +servers: + - url: https://api.example.com/v2 + description: Main production server +paths: + /pets: + get: + tags: + - pets + summary: List all pets + operationId: listPets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + schema: + exclusiveMaximum: 100 + exclusiveMinimum: 1 + type: integer + responses: + '200': + description: A paged array of pets + content: + application/json: + schema: + $ref: https://example.com/schemas/pet.json + /sample: + get: + summary: Sample endpoint + responses: + '200': + description: Sample response + content: + application/json: + schema: + $id: https://example.com/schemas/person.schema.yaml + $schema: https://json-schema.org/draft/2020-12/schema + $comment: A schema defining a pet object with optional references to dynamic components. + $vocabulary: + https://json-schema.org/draft/2020-12/vocab/core: true + https://json-schema.org/draft/2020-12/vocab/applicator: true + https://json-schema.org/draft/2020-12/vocab/validation: true + https://json-schema.org/draft/2020-12/vocab/meta-data: false + https://json-schema.org/draft/2020-12/vocab/format-annotation: false + $dynamicAnchor: addressDef + title: Pet + required: + - name + type: object + properties: + name: + $comment: The pet's full name + type: string + address: + $comment: Reference to an address definition which can change dynamically + $dynamicRef: '#addressDef' + description: Schema for a pet object +components: + schemas: + Pet: + $id: https://example.com/schemas/pet.json + $comment: This schema represents a pet in the system. + $defs: + ExtraInfo: + type: string + required: + - id + - weight + type: object + properties: + id: + type: string + format: uuid + weight: + exclusiveMinimum: 0 + type: number + description: Weight of the pet in kilograms + attributes: + patternProperties: + '^attr_[A-Za-z]+$': + type: string + type: + - 'null' + - object + description: Dynamic attributes for the pet + securitySchemes: + api_key: + type: apiKey + name: api_key + in: header +security: + - api_key: [ ] +tags: + - name: pets +webhooks: + newPetAlert: + post: + summary: Notify about a new pet being added + requestBody: + content: + application/json: + schema: + type: string + required: true + responses: + '200': + description: Webhook processed successfully diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs new file mode 100644 index 000000000..aae922700 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs @@ -0,0 +1,616 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Tests; +using Xunit; +using VerifyXunit; +using System; +using System.Net.Http; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests +{ + public class OpenApiDocumentTests + { + private const string SampleFolderPath = "V32Tests/Samples/OpenApiDocument/"; + + [Fact] + public async Task ParseDocumentWithWebhooksShouldSucceed() + { + // Arrange and Act + var actual = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "documentWithWebhooks.yaml"), SettingsFixture.ReaderSettings); + var petSchema = new OpenApiSchemaReference("petSchema", actual.Document); + + var newPetSchema = new OpenApiSchemaReference("newPetSchema", actual.Document); + + var components = new OpenApiComponents + { + Schemas = new Dictionary() + { + ["petSchema"] = new OpenApiSchema() + { + Type = JsonSchemaType.Object, + Required = new HashSet + { + "id", + "name" + }, + DependentRequired = new Dictionary> + { + { "tag", new HashSet { "category" } } + }, + Properties = new Dictionary() + { + ["id"] = new OpenApiSchema() + { + Type = JsonSchemaType.Integer, + Format = "int64" + }, + ["name"] = new OpenApiSchema() + { + Type = JsonSchemaType.String + }, + ["tag"] = new OpenApiSchema() + { + Type = JsonSchemaType.String + }, + ["category"] = new OpenApiSchema() + { + Type = JsonSchemaType.String, + }, + } + }, + ["newPetSchema"] = new OpenApiSchema() + { + Type = JsonSchemaType.Object, + Required = new HashSet + { + "name" + }, + DependentRequired = new Dictionary> + { + { "tag", new HashSet { "category" } } + }, + Properties = new Dictionary() + { + ["id"] = new OpenApiSchema() + { + Type = JsonSchemaType.Integer, + Format = "int64" + }, + ["name"] = new OpenApiSchema() + { + Type = JsonSchemaType.String + }, + ["tag"] = new OpenApiSchema() + { + Type = JsonSchemaType.String + }, + ["category"] = new OpenApiSchema() + { + Type = JsonSchemaType.String, + }, + } + } + } + }; + + var expected = new OpenApiDocument + { + Info = new OpenApiInfo + { + Version = "1.0.0", + Title = "Webhook Example" + }, + Webhooks = new Dictionary + { + ["pets"] = new OpenApiPathItem + { + Operations = new() + { + [HttpMethod.Get] = new OpenApiOperation + { + Description = "Returns all pets from the system that the user has access to", + OperationId = "findPets", + Parameters = + [ + new OpenApiParameter + { + Name = "tags", + In = ParameterLocation.Query, + Description = "tags to filter by", + Required = false, + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Array, + Items = new OpenApiSchema() + { + Type = JsonSchemaType.String + } + } + }, + new OpenApiParameter + { + Name = "limit", + In = ParameterLocation.Query, + Description = "maximum number of results to return", + Required = false, + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Integer, + Format = "int32" + } + } + ], + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "pet response", + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Array, + Items = petSchema + } + }, + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Array, + Items = petSchema + } + } + } + } + } + }, + [HttpMethod.Post] = new OpenApiOperation + { + RequestBody = new OpenApiRequestBody + { + Description = "Information about a new pet in the system", + Required = true, + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = newPetSchema + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "Return a 200 status to indicate that the data was received successfully", + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = petSchema + } + } + } + } + } + } + } + }, + Components = components + }; + + // Assert + Assert.Equivalent(new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_2, Format = OpenApiConstants.Yaml }, actual.Diagnostic); + actual.Document.Should().BeEquivalentTo(expected, options => options.Excluding(x => x.Workspace).Excluding(y => y.BaseUri)); + } + + [Fact] + public async Task ParseDocumentsWithReusablePathItemInWebhooksSucceeds() + { + // Arrange && Act + var actual = await OpenApiDocument.LoadAsync("V32Tests/Samples/OpenApiDocument/documentWithReusablePaths.yaml", SettingsFixture.ReaderSettings); + + var components = new OpenApiComponents + { + Schemas = new Dictionary() + { + ["petSchema"] = new OpenApiSchema() + { + Type = JsonSchemaType.Object, + Required = new HashSet + { + "id", + "name" + }, + DependentRequired = new Dictionary> + { + { "tag", new HashSet { "category" } } + }, + Properties = new Dictionary() + { + ["id"] = new OpenApiSchema() + { + Type = JsonSchemaType.Integer, + Format = "int64" + }, + ["name"] = new OpenApiSchema() + { + Type = JsonSchemaType.String + }, + ["tag"] = new OpenApiSchema() + { + Type = JsonSchemaType.String + }, + ["category"] = new OpenApiSchema() + { + Type = JsonSchemaType.String, + }, + } + }, + ["newPetSchema"] = new OpenApiSchema() + { + Type = JsonSchemaType.Object, + Required = new HashSet + { + "name" + }, + DependentRequired = new Dictionary> + { + { "tag", new HashSet { "category" } } + }, + Properties = new Dictionary() + { + ["id"] = new OpenApiSchema() + { + Type = JsonSchemaType.Integer, + Format = "int64" + }, + ["name"] = new OpenApiSchema() + { + Type = JsonSchemaType.String + }, + ["tag"] = new OpenApiSchema() + { + Type = JsonSchemaType.String + }, + ["category"] = new OpenApiSchema() + { + Type = JsonSchemaType.String, + }, + } + } + } + }; + + // Create a clone of the schema to avoid modifying things in components. + var petSchema = new OpenApiSchemaReference("petSchema", actual.Document); + + var newPetSchema = new OpenApiSchemaReference("newPetSchema", actual.Document); + + components.PathItems = new Dictionary + { + ["pets"] = new OpenApiPathItem + { + Operations = new() + { + [HttpMethod.Get] = new OpenApiOperation + { + Description = "Returns all pets from the system that the user has access to", + OperationId = "findPets", + Parameters = + [ + new OpenApiParameter + { + Name = "tags", + In = ParameterLocation.Query, + Description = "tags to filter by", + Required = false, + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Array, + Items = new OpenApiSchema() + { + Type = JsonSchemaType.String + } + } + }, + new OpenApiParameter + { + Name = "limit", + In = ParameterLocation.Query, + Description = "maximum number of results to return", + Required = false, + Schema = new OpenApiSchema() + { + Type = JsonSchemaType.Integer, + Format = "int32" + } + } + ], + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "pet response", + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = JsonSchemaType.Array, + Items = petSchema + } + }, + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = JsonSchemaType.Array, + Items = petSchema + } + } + } + } + } + }, + [HttpMethod.Post] = new OpenApiOperation + { + RequestBody = new OpenApiRequestBody + { + Description = "Information about a new pet in the system", + Required = true, + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = newPetSchema + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "Return a 200 status to indicate that the data was received successfully", + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = petSchema + }, + } + } + } + } + } + } + }; + + var expected = new OpenApiDocument + { + Info = new OpenApiInfo + { + Title = "Webhook Example", + Version = "1.0.0" + }, + JsonSchemaDialect = new Uri("http://json-schema.org/draft-07/schema#"), + Webhooks = new Dictionary + { + ["pets"] = components.PathItems["pets"] + }, + Components = components + }; + + // Assert + actual.Document.Should().BeEquivalentTo(expected, options => options + .Excluding(x => x.Workspace) + .Excluding(y => y.BaseUri)); + Assert.Equivalent( + new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_2, Format = OpenApiConstants.Yaml }, actual.Diagnostic); + } + + [Fact] + public async Task ParseDocumentWithExampleInSchemaShouldSucceed() + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = false }); + + // Act + var actual = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "docWithExample.yaml"), SettingsFixture.ReaderSettings); + actual.Document.SerializeAsV32(writer); + + // Assert + Assert.NotNull(actual); + } + + [Fact] + public async Task ParseDocumentWithPatternPropertiesInSchemaWorks() + { + // Arrange and Act + var result = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "docWithPatternPropertiesInSchema.yaml"), SettingsFixture.ReaderSettings); + var actualSchema = result.Document.Paths["/example"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + var expectedSchema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["prop1"] = new OpenApiSchema + { + Type = JsonSchemaType.String + }, + ["prop2"] = new OpenApiSchema + { + Type = JsonSchemaType.String + }, + ["prop3"] = new OpenApiSchema + { + Type = JsonSchemaType.String + } + }, + PatternProperties = new Dictionary() + { + ["^x-.*$"] = new OpenApiSchema + { + Type = JsonSchemaType.String + } + } + }; + + // Serialization + var mediaType = result.Document.Paths["/example"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"]; + + var expectedMediaType = @"schema: + patternProperties: + ^x-.*$: + type: string + type: object + properties: + prop1: + type: string + prop2: + type: string + prop3: + type: string"; + + var actualMediaType = await mediaType.SerializeAsYamlAsync(OpenApiSpecVersion.OpenApi3_2); + + // Assert + Assert.Equivalent(expectedSchema, actualSchema); + Assert.Equal(expectedMediaType.MakeLineBreaksEnvironmentNeutral(), actualMediaType.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public async Task ParseDocumentWithReferenceByIdGetsResolved() + { + // Arrange and Act + var result = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "docWithReferenceById.yaml"), SettingsFixture.ReaderSettings); + + var responseSchema = result.Document.Paths["/resource"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + var requestBodySchema = result.Document.Paths["/resource"].Operations[HttpMethod.Post].RequestBody.Content["application/json"].Schema; + var parameterSchema = result.Document.Paths["/resource"].Operations[HttpMethod.Get].Parameters[0].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, responseSchema.Type); + Assert.Equal(JsonSchemaType.Object, requestBodySchema.Type); + Assert.Equal(JsonSchemaType.String, parameterSchema.Type); + } + + [Fact] + public async Task ExternalDocumentDereferenceToOpenApiDocumentUsingJsonPointerWorks() + { + // Arrange + var documentName = "externalRefByJsonPointer.yaml"; + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath, documentName); + + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var result = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, documentName), settings); + var responseSchema = result.Document.Paths["/resource"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + var externalResourceUri = new Uri( + "file://" + + Path.Combine(Path.GetFullPath(SampleFolderPath), + "externalResource.yaml#/components/schemas/todo")).AbsoluteUri; + + Assert.True(result.Document.Workspace.Contains(externalResourceUri)); + Assert.Equal(2, responseSchema.Properties.Count); // reference has been resolved + } + + [Fact] + public async Task ParseExternalDocumentDereferenceToOpenApiDocumentByIdWorks() + { + // Arrange + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var result = await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalRefById.yaml"), settings); + var doc2 = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalResource.yaml"), SettingsFixture.ReaderSettings)).Document; + + var requestBodySchema = result.Document.Paths["/resource"].Operations[HttpMethod.Get].Parameters[0].Schema; + result.Document.Workspace.RegisterComponents(doc2); + + // Assert + Assert.Equal(2, requestBodySchema.Properties.Count); // reference has been resolved + } + + [Fact] + public async Task ParseDocumentWith32PropertiesWorks() + { + var path = Path.Combine(SampleFolderPath, "documentWith32Properties.yaml"); + var doc = (await OpenApiDocument.LoadAsync(path, SettingsFixture.ReaderSettings)).Document; + var outputStringWriter = new StringWriter(); + doc.SerializeAsV32(new OpenApiYamlWriter(outputStringWriter)); + await outputStringWriter.FlushAsync(); + var actual = outputStringWriter.GetStringBuilder().ToString(); + + // Assert + await Verifier.Verify(actual); + } + + [Fact] + public async Task ParseDocumentWithEmptyTagsWorks() + { + var path = Path.Combine(SampleFolderPath, "documentWithEmptyTags.json"); + var doc = (await OpenApiDocument.LoadAsync(path, SettingsFixture.ReaderSettings)).Document; + + doc.Paths["/groups"].Operations[HttpMethod.Get].Tags.Should().BeNull("Empty tags are ignored, so we should not have any tags"); + } + [Fact] + public async Task DocumentWithSchemaResultsInWarning() + { + var path = Path.Combine(SampleFolderPath, "documentWithSchema.json"); + var (doc, diag) = await OpenApiDocument.LoadAsync(path, SettingsFixture.ReaderSettings); + Assert.NotNull(doc); + Assert.NotNull(diag); + Assert.Empty(diag.Errors); + Assert.Single(diag.Warnings); + Assert.StartsWith("$schema is not a valid property", diag.Warnings[0].Message); + } + + [Fact] + public void ParseEmptyMemoryStreamThrowsAnArgumentException() + { + Assert.Throws(() => OpenApiDocument.Load(new MemoryStream())); + } + + [Fact] + public async Task ValidateReferencedExampleInSchemaWorks() + { + // Arrange && Act + var path = Path.Combine(SampleFolderPath, "docWithReferencedExampleInSchemaWorks.yaml"); + var result = await OpenApiDocument.LoadAsync(path, SettingsFixture.ReaderSettings); + var actualSchemaExample = result.Document.Components.Schemas["DiffCreatedEvent"].Properties["updatedAt"].Example; + var targetSchemaExample = result.Document.Components.Schemas["Timestamp"].Example; + + // Assert + Assert.Equal(targetSchemaExample, actualSchemaExample); + Assert.Empty(result.Diagnostic.Errors); + Assert.Empty(result.Diagnostic.Warnings); + } + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiExampleReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiExampleReferenceDeserializerTests.cs new file mode 100644 index 000000000..c32460cba --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiExampleReferenceDeserializerTests.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiExampleReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializeExampleReferenceAnnotations() + { + var json = + """ + { + "$ref": "#/components/examples/MyExample", + "description": "This is an example reference", + "summary": "Example Summary reference" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.AddComponent("MyExample", new OpenApiExample + { + Summary = "This is an example", + Description = "This is an example description", + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadExample(parseNode, hostDocument); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.Equal("MyExample", resultReference.Reference.Id); + Assert.Equal("This is an example reference", resultReference.Description); + Assert.Equal("Example Summary reference", resultReference.Summary); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiHeaderReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiHeaderReferenceDeserializerTests.cs new file mode 100644 index 000000000..abe5146c4 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiHeaderReferenceDeserializerTests.cs @@ -0,0 +1,40 @@ + +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiHeaderReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializeReferenceAnnotations() + { + var json = + """ + { + "$ref": "#/components/headers/MyHeader", + "description": "This is a header reference" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.AddComponent("MyHeader", new OpenApiHeader + { + Description = "This is a header" + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadHeader(parseNode, hostDocument); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.Equal("MyHeader", resultReference.Reference.Id); + Assert.Equal("This is a header reference", resultReference.Description); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiInfoTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiInfoTests.cs new file mode 100644 index 000000000..aa578e200 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiInfoTests.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Microsoft.OpenApi.YamlReader; +using SharpYaml.Serialization; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests +{ + public class OpenApiInfoTests + { + private const string SampleFolderPath = "V32Tests/Samples/OpenApiInfo/"; + + [Fact] + public void ParseBasicInfoShouldSucceed() + { + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "basicInfo.yaml")); + var yamlStream = new YamlStream(); + yamlStream.Load(new StreamReader(stream)); + var yamlNode = yamlStream.Documents[0].RootNode; + + var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); + + var asJsonNode = yamlNode.ToJsonNode(); + var node = new MapNode(context, asJsonNode); + + // Act + var openApiInfo = OpenApiV32Deserializer.LoadInfo(node, new()); + + // Assert + Assert.Equivalent( + new OpenApiInfo + { + Title = "Basic Info", + Summary = "Sample Summary", + Description = "Sample Description", + Version = "1.0.1", + TermsOfService = new Uri("http://swagger.io/terms/"), + Contact = new OpenApiContact + { + Email = "support@swagger.io", + Name = "API Support", + Url = new Uri("http://www.swagger.io/support") + }, + License = new OpenApiLicense + { + Name = "Apache 2.0", + Url = new Uri("http://www.apache.org/licenses/LICENSE-2.0.html") + } + }, openApiInfo); + } + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiLicenseTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiLicenseTests.cs new file mode 100644 index 000000000..94515b9e7 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiLicenseTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.IO; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Microsoft.OpenApi.YamlReader; +using SharpYaml.Serialization; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests +{ + + public class OpenApiLicenseTests + { + private const string SampleFolderPath = "V32Tests/Samples/OpenApiLicense/"; + + [Fact] + public void ParseLicenseWithSpdxIdentifierShouldSucceed() + { + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "licenseWithSpdxIdentifier.yaml")); + var yamlStream = new YamlStream(); + yamlStream.Load(new StreamReader(stream)); + var yamlNode = yamlStream.Documents[0].RootNode; + + var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); + + var asJsonNode = yamlNode.ToJsonNode(); + var node = new MapNode(context, asJsonNode); + + // Act + var license = OpenApiV32Deserializer.LoadLicense(node, new()); + + // Assert + Assert.Equivalent( + new OpenApiLicense + { + Name = "Apache 2.0", + Identifier = "Apache-2.0" + }, license); + } + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiLinkReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiLinkReferenceDeserializerTests.cs new file mode 100644 index 000000000..4716d05fb --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiLinkReferenceDeserializerTests.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiLinkReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializeLinkReferenceAnnotations() + { + var json = + """ + { + "$ref": "#/components/links/MyLink", + "description": "This is a link reference" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.AddComponent("MyLink", new OpenApiLink + { + Description = "This is a link description", + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadLink(parseNode, hostDocument); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.Equal("MyLink", resultReference.Reference.Id); + Assert.Equal("This is a link reference", resultReference.Description); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiParameterReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiParameterReferenceDeserializerTests.cs new file mode 100644 index 000000000..6f4f8b4cc --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiParameterReferenceDeserializerTests.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiParameterReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializeParameterReferenceAnnotations() + { + var json = + """ + { + "$ref": "#/components/parameters/MyParameter", + "description": "This is a parameter reference" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.AddComponent("MyParameter", new OpenApiParameter + { + Name = "myParam", + In = ParameterLocation.Query, + Description = "This is a parameter description", + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadParameter(parseNode, hostDocument); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.Equal("MyParameter", resultReference.Reference.Id); + Assert.Equal("This is a parameter reference", resultReference.Description); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiPathItemReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiPathItemReferenceDeserializerTests.cs new file mode 100644 index 000000000..76af3ce00 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiPathItemReferenceDeserializerTests.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiPathItemReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializePathItemReferenceAnnotations() + { + var json = + """ + { + "$ref": "#/components/pathItems/MyPathItem", + "description": "This is a path item reference", + "summary": "PathItem Summary reference" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.AddComponent("MyPathItem", new OpenApiPathItem + { + Summary = "This is a path item", + Description = "This is a path item description", + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadPathItem(parseNode, hostDocument); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.Equal("MyPathItem", resultReference.Reference.Id); + Assert.Equal("This is a path item reference", resultReference.Description); + Assert.Equal("PathItem Summary reference", resultReference.Summary); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiRequestBodyReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiRequestBodyReferenceDeserializerTests.cs new file mode 100644 index 000000000..cda54fc9e --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiRequestBodyReferenceDeserializerTests.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiRequestBodyReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializeRequestBodyReferenceAnnotations() + { + var json = + """ + { + "$ref": "#/components/requestBodies/MyRequestBody", + "description": "This is a request body reference" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.AddComponent("MyRequestBody", new OpenApiRequestBody + { + Description = "This is a request body description", + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadRequestBody(parseNode, hostDocument); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.Equal("MyRequestBody", resultReference.Reference.Id); + Assert.Equal("This is a request body reference", resultReference.Description); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiResponseReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiResponseReferenceDeserializerTests.cs new file mode 100644 index 000000000..eac03d63b --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiResponseReferenceDeserializerTests.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiResponseReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializeResponseReferenceAnnotations() + { + var json = + """ + { + "$ref": "#/components/responses/MyResponse", + "description": "This is a response reference" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.AddComponent("MyResponse", new OpenApiResponse + { + Description = "This is a response description", + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadResponse(parseNode, hostDocument); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.Equal("MyResponse", resultReference.Reference.Id); + Assert.Equal("This is a response reference", resultReference.Description); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs new file mode 100644 index 000000000..bb7b15ab0 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaReferenceDeserializerTests.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiSchemaReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializeSchemaReferenceAnnotations() + { + var json = + """ + { + "$ref": "#/components/schemas/MySchema", + "description": "This is a schema reference", + "default": "foo", + "readOnly": true, + "writeOnly": true, + "deprecated": true, + "title": "This is a schema reference", + "examples": ["example reference value"] + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.AddComponent("MySchema", new OpenApiSchema + { + Title = "This is a schema", + Description = "This is a schema description", + Default = "bar", + Type = JsonSchemaType.String, + ReadOnly = false, + WriteOnly = false, + Deprecated = false, + Examples = new List { "example value" }, + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadSchema(parseNode, hostDocument); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.Equal("MySchema", resultReference.Reference.Id); + Assert.Equal("This is a schema reference", resultReference.Description); + Assert.Equal("foo", resultReference.Default?.ToString()); + Assert.True(resultReference.ReadOnly); + Assert.True(resultReference.WriteOnly); + Assert.True(resultReference.Deprecated); + Assert.Equal("This is a schema reference", resultReference.Title); + Assert.NotNull(resultReference.Examples); + Assert.Single(resultReference.Examples); + Assert.Equal("example reference value", resultReference.Examples[0]?.ToString()); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs new file mode 100644 index 000000000..73be91f10 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs @@ -0,0 +1,614 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Equivalency; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Tests; +using Xunit; +using System; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests +{ + public class OpenApiSchemaTests + { + private const string SampleFolderPath = "V32Tests/Samples/OpenApiSchema/"; + + + public static MemoryStream GetMemoryStream(string fileName) + { + var filePath = Path.Combine(SampleFolderPath, fileName); + var fileBytes = File.ReadAllBytes(filePath); + return new MemoryStream(fileBytes); + } + + [Fact] + public async Task ParseBasicV32SchemaShouldSucceed() + { + var expectedObject = new OpenApiSchema() + { + Id = "https://example.com/arrays.schema.json", + Schema = new Uri("https://json-schema.org/draft/2020-12/schema"), + Description = "A representation of a person, company, organization, or place", + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["fruits"] = new OpenApiSchema + { + Type = JsonSchemaType.Array, + Items = new OpenApiSchema + { + Type = JsonSchemaType.String + } + }, + ["vegetables"] = new OpenApiSchema + { + Type = JsonSchemaType.Array + } + }, + Definitions = new Dictionary + { + ["veggie"] = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Required = new HashSet + { + "veggieName", + "veggieLike" + }, + DependentRequired = new Dictionary> + { + { "veggieType", new HashSet { "veggieColor", "veggieSize" } } + }, + Properties = new Dictionary() + { + ["veggieName"] = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "The name of the vegetable." + }, + ["veggieLike"] = new OpenApiSchema + { + Type = JsonSchemaType.Boolean, + Description = "Do I like this vegetable?" + }, + ["veggieType"] = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "The type of vegetable (e.g., root, leafy, etc.)." + }, + ["veggieColor"] = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "The color of the vegetable." + }, + ["veggieSize"] = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "The size of the vegetable." + } + } + } + } + }; + + // Act + var schema = await OpenApiModelFactory.LoadAsync( + Path.Combine(SampleFolderPath, "jsonSchema.json"), OpenApiSpecVersion.OpenApi3_2, new(), SettingsFixture.ReaderSettings); + + // Assert + Assert.Equivalent(expectedObject, schema); + } + + [Fact] + public void ParseSchemaWithTypeArrayWorks() + { + // Arrange + var schema = @"{ + ""$id"": ""https://example.com/arrays.schema.json"", + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""description"": ""A representation of a person, company, organization, or place"", + ""type"": [""object"", ""null""] +}"; + + var expected = new OpenApiSchema() + { + Id = "https://example.com/arrays.schema.json", + Schema = new Uri("https://json-schema.org/draft/2020-12/schema"), + Description = "A representation of a person, company, organization, or place", + Type = JsonSchemaType.Object | JsonSchemaType.Null + }; + + // Act + var actual = OpenApiModelFactory.Parse(schema, OpenApiSpecVersion.OpenApi3_2, new(), out _); + + // Assert + Assert.Equivalent(expected, actual); + } + + [Fact] + public void TestSchemaCopyConstructorWithTypeArrayWorks() + { + /* Arrange + * Test schema's copy constructor for deep-cloning type array + */ + var schemaWithTypeArray = new OpenApiSchema() + { + Type = JsonSchemaType.Array | JsonSchemaType.Null, + Items = new OpenApiSchema + { + Type = JsonSchemaType.String + } + }; + + var simpleSchema = new OpenApiSchema() + { + Type = JsonSchemaType.String + }; + + // Act + var schemaWithArrayCopy = schemaWithTypeArray.CreateShallowCopy() as OpenApiSchema; + schemaWithArrayCopy.Type = JsonSchemaType.String; + + var simpleSchemaCopy = simpleSchema.CreateShallowCopy() as OpenApiSchema; + simpleSchemaCopy.Type = JsonSchemaType.String | JsonSchemaType.Null; + + // Assert + Assert.NotEqual(schemaWithTypeArray.Type, schemaWithArrayCopy.Type); + schemaWithTypeArray.Type = JsonSchemaType.String | JsonSchemaType.Null; + + Assert.NotEqual(simpleSchema.Type, simpleSchemaCopy.Type); + simpleSchema.Type = JsonSchemaType.String; + } + + [Fact] + public async Task ParseV32SchemaShouldSucceed() + { + var path = Path.Combine(SampleFolderPath, "schema.yaml"); + + // Act + var schema = await OpenApiModelFactory.LoadAsync(path, OpenApiSpecVersion.OpenApi3_2, new(), SettingsFixture.ReaderSettings); + var expectedSchema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["one"] = new OpenApiSchema() + { + Description = "type array", + Type = JsonSchemaType.Integer | JsonSchemaType.String + } + } + }; + + // Assert + Assert.Equivalent(expectedSchema, schema); + } + + [Fact] + public async Task ParseAdvancedV32SchemaShouldSucceed() + { + // Arrange and Act + var path = Path.Combine(SampleFolderPath, "advancedSchema.yaml"); + var schema = await OpenApiModelFactory.LoadAsync(path, OpenApiSpecVersion.OpenApi3_2, new(), SettingsFixture.ReaderSettings); + + var expectedSchema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary() + { + ["one"] = new OpenApiSchema() + { + Description = "type array", + Type = JsonSchemaType.Integer | JsonSchemaType.String + }, + ["two"] = new OpenApiSchema() + { + Description = "type 'null'", + Type = JsonSchemaType.Null + }, + ["three"] = new OpenApiSchema() + { + Description = "type array including 'null'", + Type = JsonSchemaType.String | JsonSchemaType.Null + }, + ["four"] = new OpenApiSchema() + { + Description = "array with no items", + Type = JsonSchemaType.Array + }, + ["five"] = new OpenApiSchema() + { + Description = "singular example", + Type = JsonSchemaType.String, + Examples = new List + { + "exampleValue" + } + }, + ["six"] = new OpenApiSchema() + { + Description = "exclusiveMinimum true", + ExclusiveMinimum = "10" + }, + ["seven"] = new OpenApiSchema() + { + Description = "exclusiveMinimum false", + Minimum = "10" + }, + ["eight"] = new OpenApiSchema() + { + Description = "exclusiveMaximum true", + ExclusiveMaximum = "20" + }, + ["nine"] = new OpenApiSchema() + { + Description = "exclusiveMaximum false", + Maximum = "20" + }, + ["ten"] = new OpenApiSchema() + { + Description = "nullable string", + Type = JsonSchemaType.String | JsonSchemaType.Null + }, + ["eleven"] = new OpenApiSchema() + { + Description = "x-nullable string", + Type = JsonSchemaType.String | JsonSchemaType.Null + }, + ["twelve"] = new OpenApiSchema() + { + Description = "file/binary" + } + } + }; + + // Assert + schema.Should().BeEquivalentTo(expectedSchema, options => options + .IgnoringCyclicReferences() + .Excluding((IMemberInfo memberInfo) => + memberInfo.Path.EndsWith("Parent"))); + } + + [Fact] + public void ParseSchemaWithExamplesShouldSucceed() + { + // Arrange + var input = @" +type: string +examples: + - fedora + - ubuntu +"; + // Act + var schema = OpenApiModelFactory.Parse(input, OpenApiSpecVersion.OpenApi3_2, new(), out _, "yaml", SettingsFixture.ReaderSettings); + + // Assert + Assert.Equal(2, schema.Examples.Count); + } + + [Fact] + public void CloningSchemaWithExamplesAndEnumsShouldSucceed() + { + // Arrange + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Integer, + Default = 5, + Examples = [2, 3], + Enum = [1, 2, 3] + }; + + var clone = schema.CreateShallowCopy() as OpenApiSchema; + clone.Examples.Add(4); + clone.Enum.Add(4); + clone.Default = 6; + + // Assert + Assert.Equivalent(new int[] { 1, 2, 3, 4 }, clone.Enum.Select(static x => x.GetValue()).ToArray()); + Assert.Equivalent(new int[] { 2, 3, 4 }, clone.Examples.Select(static x => x.GetValue()).ToArray()); + Assert.Equivalent(6, clone.Default.GetValue()); + } + + [Fact] + public async Task SerializeV32SchemaWithMultipleTypesAsV3Works() + { + // Arrange + var expected = @"type: string +nullable: true"; + + var path = Path.Combine(SampleFolderPath, "schemaWithTypeArray.yaml"); + + // Act + var schema = await OpenApiModelFactory.LoadAsync(path, OpenApiSpecVersion.OpenApi3_2, new(), SettingsFixture.ReaderSettings); + + var writer = new StringWriter(); + schema.SerializeAsV3(new OpenApiYamlWriter(writer)); + var schema1String = writer.ToString(); + + Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schema1String.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public async Task SerializeV32SchemaWithMultipleTypesAsV2Works() + { + // Arrange + var expected = @"type: string +x-nullable: true"; + + var path = Path.Combine(SampleFolderPath, "schemaWithTypeArray.yaml"); + + // Act + var schema = await OpenApiModelFactory.LoadAsync(path, OpenApiSpecVersion.OpenApi3_2, new(), SettingsFixture.ReaderSettings); + + var writer = new StringWriter(); + schema.SerializeAsV2(new OpenApiYamlWriter(writer)); + var schema1String = writer.ToString(); + + Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schema1String.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public async Task SerializeV3SchemaWithNullableAsV32Works() + { + // Arrange + var expected = @"type: + - 'null' + - string"; + + var path = Path.Combine(SampleFolderPath, "schemaWithNullable.yaml"); + + // Act + var schema = await OpenApiModelFactory.LoadAsync(path, OpenApiSpecVersion.OpenApi3_0, new(), SettingsFixture.ReaderSettings); + + var writer = new StringWriter(); + schema.SerializeAsV32(new OpenApiYamlWriter(writer)); + var schemaString = writer.ToString(); + + Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schemaString.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public async Task SerializeV2SchemaWithNullableExtensionAsV32Works() + { + // Arrange + var expected = @"type: + - 'null' + - string"; + + var path = Path.Combine(SampleFolderPath, "schemaWithNullableExtension.yaml"); + + // Act + var schema = await OpenApiModelFactory.LoadAsync(path, OpenApiSpecVersion.OpenApi2_0, new(), SettingsFixture.ReaderSettings); + + var writer = new StringWriter(); + schema.SerializeAsV32(new OpenApiYamlWriter(writer)); + var schemaString = writer.ToString(); + + Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schemaString.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public void SerializeSchemaWithTypeArrayAndNullableDoesntEmitType() + { + var input = @"type: +- ""string"" +- ""int"" +nullable: true"; + + var expected = @"x-nullable: true"; + + var schema = OpenApiModelFactory.Parse(input, OpenApiSpecVersion.OpenApi3_2, new(), out _, "yaml", SettingsFixture.ReaderSettings); + + var writer = new StringWriter(); + schema.SerializeAsV2(new OpenApiYamlWriter(writer)); + var schemaString = writer.ToString(); + + Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schemaString.MakeLineBreaksEnvironmentNeutral()); + } + + [Theory] + [InlineData("schemaWithNullable.yaml")] + [InlineData("schemaWithNullableExtension.yaml")] + public async Task LoadSchemaWithNullableExtensionAsV32Works(string filePath) + { + // Arrange + var path = Path.Combine(SampleFolderPath, filePath); + + // Act + var schema = await OpenApiModelFactory.LoadAsync(path, OpenApiSpecVersion.OpenApi3_2, new(), SettingsFixture.ReaderSettings); + + // Assert + Assert.Equal(JsonSchemaType.String | JsonSchemaType.Null, schema.Type); + } + + [Fact] + public async Task SerializeSchemaWithJsonSchemaKeywordsWorks() + { + // Arrange + var expected = @"$id: https://example.com/schemas/person.schema.yaml +$schema: https://json-schema.org/draft/2020-12/schema +$comment: A schema defining a person object with optional references to dynamic components. +$vocabulary: + https://json-schema.org/draft/2020-12/vocab/core: true + https://json-schema.org/draft/2020-12/vocab/applicator: true + https://json-schema.org/draft/2020-12/vocab/validation: true + https://json-schema.org/draft/2020-12/vocab/meta-data: false + https://json-schema.org/draft/2020-12/vocab/format-annotation: false +$dynamicAnchor: addressDef +title: Person +required: + - name +type: object +properties: + name: + $comment: The person's full name + type: string + age: + $comment: Age must be a non-negative integer + minimum: 0 + type: integer + address: + $comment: Reference to an address definition which can change dynamically + $dynamicRef: '#addressDef' +description: Schema for a person object +"; + var path = Path.Combine(SampleFolderPath, "schemaWithJsonSchemaKeywords.yaml"); + + // Act + var schema = await OpenApiModelFactory.LoadAsync(path, OpenApiSpecVersion.OpenApi3_2, new(), SettingsFixture.ReaderSettings); + + // serialization + var writer = new StringWriter(); + schema.SerializeAsV32(new OpenApiYamlWriter(writer)); + var schemaString = writer.ToString(); + + // Assert + Assert.Equal(5, schema.Vocabulary.Keys.Count); + Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schemaString.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public async Task ParseSchemaWithConstWorks() + { + var expected = @"{ + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""required"": [ + ""status"" + ], + ""type"": ""object"", + ""properties"": { + ""status"": { + ""const"": ""active"", + ""type"": ""string"" + }, + ""user"": { + ""required"": [ + ""role"" + ], + ""type"": ""object"", + ""properties"": { + ""role"": { + ""const"": ""admin"", + ""type"": ""string"" + } + } + } + } +}"; + + var path = Path.Combine(SampleFolderPath, "schemaWithConst.json"); + + // Act + var schema = await OpenApiModelFactory.LoadAsync(path, OpenApiSpecVersion.OpenApi3_2, new(), SettingsFixture.ReaderSettings); + Assert.Equal("active", schema.Properties["status"].Const); + Assert.Equal("admin", schema.Properties["user"].Properties["role"].Const); + + // serialization + var writer = new StringWriter(); + schema.SerializeAsV32(new OpenApiJsonWriter(writer)); + var schemaString = writer.ToString(); + Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schemaString.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public void ParseSchemaWithUnrecognizedKeywordsWorks() + { + var input = @"{ + ""type"": ""string"", + ""format"": ""date-time"", + ""customKeyword"": ""customValue"", + ""anotherKeyword"": 42, + ""x-test"": ""test"" +} +"; + var schema = OpenApiModelFactory.Parse(input, OpenApiSpecVersion.OpenApi3_2, new(), out _, "json"); + Assert.Equal(2, schema.UnrecognizedKeywords.Count); + } + + [Fact] + public void ParseSchemaExampleWithPrimitivesWorks() + { + var expected1 = @"{ + ""type"": ""string"", + ""example"": ""2024-01-02"" +}"; + + var expected2 = @"{ + ""type"": ""string"", + ""example"": ""3.24"" +}"; + var schema = new OpenApiSchema() + { + Type = JsonSchemaType.String, + Example = JsonValue.Create("2024-01-02") + }; + + var schema2 = new OpenApiSchema() + { + Type = JsonSchemaType.String, + Example = JsonValue.Create("3.24") + }; + + var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + schema.SerializeAsV32(writer); + var actual1 = textWriter.ToString(); + Assert.Equal(expected1.MakeLineBreaksEnvironmentNeutral(), actual1.MakeLineBreaksEnvironmentNeutral()); + + textWriter = new StringWriter(); + writer = new OpenApiJsonWriter(textWriter); + schema2.SerializeAsV32(writer); + var actual2 = textWriter.ToString(); + Assert.Equal(expected2.MakeLineBreaksEnvironmentNeutral(), actual2.MakeLineBreaksEnvironmentNeutral()); + } + + [Theory] + [InlineData(JsonSchemaType.Integer | JsonSchemaType.String, new[] { "integer", "string" })] + [InlineData(JsonSchemaType.Integer | JsonSchemaType.Null, new[] { "integer", "null" })] + [InlineData(JsonSchemaType.Integer, new[] { "integer" })] + public void NormalizeFlaggableJsonSchemaTypeEnumWorks(JsonSchemaType type, string[] expected) + { + var schema = new OpenApiSchema + { + Type = type + }; + + var actual = schema.Type.ToIdentifiers(); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(new[] { "integer", "string" }, JsonSchemaType.Integer | JsonSchemaType.String)] + [InlineData(new[] { "integer", "null" }, JsonSchemaType.Integer | JsonSchemaType.Null)] + [InlineData(new[] { "integer" }, JsonSchemaType.Integer)] + public void ArrayIdentifierToEnumConversionWorks(string[] type, JsonSchemaType expected) + { + var actual = type.ToJsonSchemaType(); + Assert.Equal(expected, actual); + } + + [Fact] + public void StringIdentifierToEnumConversionWorks() + { + var actual = "integer".ToJsonSchemaType(); + Assert.Equal(JsonSchemaType.Integer, actual); + } + + [Fact] + public void ReturnSingleIdentifierWorks() + { + var type = JsonSchemaType.Integer; + var types = JsonSchemaType.Integer | JsonSchemaType.Null; + + Assert.Equal("integer", type.ToSingleIdentifier()); + Assert.Throws(() => types.ToSingleIdentifier()); + } + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeReferenceDeserializerTests.cs new file mode 100644 index 000000000..82d85926d --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeReferenceDeserializerTests.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiSecuritySchemeReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializeSecuritySchemeReferenceAnnotations() + { + var json = + """ + { + "$ref": "#/components/securitySchemes/MyScheme", + "description": "This is a security scheme reference" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.AddComponent("MyScheme", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.ApiKey, + Name = "api_key", + In = ParameterLocation.Header, + Description = "This is a security scheme description", + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadSecurityScheme(parseNode, hostDocument); + + Assert.NotNull(result); + var resultReference = Assert.IsType(result); + + Assert.Equal("MyScheme", resultReference.Reference.Id); + Assert.Equal("This is a security scheme reference", resultReference.Description); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiTagReferenceDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiTagReferenceDeserializerTests.cs new file mode 100644 index 000000000..6aa3b356a --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiTagReferenceDeserializerTests.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiTagReferenceDeserializerTests +{ + [Fact] + public void ShouldDeserializeTagReferenceAnnotations() + { + var json = + """ + { + "tags" : [ + "MyTag" + ] + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.Tags ??= new HashSet(); + hostDocument.Tags.Add(new OpenApiTag + { + Name = "MyTag", + Description = "This is a tag description", + }); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadOperation(parseNode, hostDocument); + // this diverges from the other unit tests because Tag References are implemented + // through the reference infrastructure for convenience, but the behave quite differently + + Assert.NotNull(result); + Assert.NotNull(result.Tags); + Assert.Single(result.Tags); + var resultReference = Assert.IsType(result.Tags.First()); + + Assert.Equal("MyTag", resultReference.Reference.Id); + Assert.Equal("This is a tag description", resultReference.Description); + Assert.NotNull(resultReference.Target); + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/OAS-schemas.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/OAS-schemas.yaml new file mode 100644 index 000000000..13f9735e4 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/OAS-schemas.yaml @@ -0,0 +1,19 @@ +openapi: 3.2.0 +info: + title: OpenAPI document containing reusable components + version: 1.0.0 +components: + schemas: + person: + type: object + properties: + name: + type: string + address: + type: object + properties: + street: + type: string + city: + type: string + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/STJSchema.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/STJSchema.json new file mode 100644 index 000000000..a938b139d --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/STJSchema.json @@ -0,0 +1,67 @@ +{ + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "format": null, + "x-schema-id": null + }, + "parent": { + "type": [ + "object", + "null" + ], + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "format": null, + "x-schema-id": null + }, + "parent": { + "$ref": "#/properties/parent", + "x-schema-id": "Category" + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "format": null, + "x-schema-id": null + } + }, + "required": [ + "name" + ], + "x-schema-id": "Tag" + } + } + }, + "required": [ + "name" + ], + "x-schema-id": "Category" + }, + "tags": { + "$ref": "#/properties/parent/properties/tags" + } + }, + "required": [ + "name" + ], + "x-schema-id": "Category" +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/componentExternalReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/componentExternalReference.yaml new file mode 100644 index 000000000..7f3404257 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/componentExternalReference.yaml @@ -0,0 +1,13 @@ +openapi: 3.2.0 +info: + title: Example of reference object in a component object + version: 1.0.0 +paths: + /item: + get: + security: + - customapikey: [] +components: + securitySchemes: + customapikey: + $ref: ./customApiKey.yaml#/components/securityschemes/customapikey diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/customApiKey.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/customApiKey.yaml new file mode 100644 index 000000000..e011543c9 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/customApiKey.yaml @@ -0,0 +1,11 @@ +openapi: 3.2.0 +info: + title: Example of reference object pointing to a parameter + version: 1.0.0 +paths: {} +components: + securitySchemes: + customapikey: + type: apiKey + name: x-api-key + in: header diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/examples.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/examples.yaml new file mode 100644 index 000000000..7285fc09b --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/examples.yaml @@ -0,0 +1,11 @@ +# file for examples (examples.yaml) +openapi: 3.2.0 +info: + title: OpenAPI document containing examples for reuse + version: 1.0.0 +components: + examples: + item-list: + value: + - name: thing + description: a thing diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/externalComponentSubschemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/externalComponentSubschemaReference.yaml new file mode 100644 index 000000000..46824675e --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/externalComponentSubschemaReference.yaml @@ -0,0 +1,14 @@ +openapi: 3.2.0 +info: + title: Reference to an external OpenApi document component + version: 1.0.0 +paths: + /person/{id}: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: 'OAS-schemas.yaml#/components/schemas/person/properties/address' diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/inlineExternalReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/inlineExternalReference.yaml new file mode 100644 index 000000000..848be1e5f --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/inlineExternalReference.yaml @@ -0,0 +1,15 @@ +openapi: 3.2.0 +info: + title: Example of reference object pointing to an example object in an OpenAPI document + version: 1.0.0 +paths: + /items: + get: + responses: + '200': + description: sample description + content: + application/json: + examples: + item-list: + $ref: './examples.yaml#/components/examples/item-list' diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/inlineLocalReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/inlineLocalReference.yaml new file mode 100644 index 000000000..e123c7e3b --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/inlineLocalReference.yaml @@ -0,0 +1,14 @@ +openapi: 3.2.0 +info: + title: Example of reference object pointing to a parameter + version: 1.0.0 +paths: + /item: + get: + parameters: + - $ref: '#/components/parameters/size' +components: + parameters: + size: + schema: + type: number diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/internalComponentReferenceUsingId.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/internalComponentReferenceUsingId.yaml new file mode 100644 index 000000000..817512702 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/internalComponentReferenceUsingId.yaml @@ -0,0 +1,29 @@ +openapi: 3.2.0 +info: + title: Reference an internal component using id + version: 1.0.0 +paths: + /person/{id}: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: 'https://schemas.acme.org/person' +components: + schemas: + person: + $id: 'https://schemas.acme.org/person' + type: object + properties: + name: + type: string + address: + type: object + properties: + street: + type: string + city: + type: string diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml new file mode 100644 index 000000000..9164dbefa --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/internalComponentsSubschemaReference.yaml @@ -0,0 +1,55 @@ +openapi: 3.2.0 +info: + title: Reference to an internal component + version: 1.0.0 +paths: + /person/{id}: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/person' + /person/{id}/address: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/person/properties/address' + /human: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/human/allOf/0' +components: + schemas: + human: + allOf: + - $ref: '#/components/schemas/person/items' + - type: object + properties: + name: + type: string + person: + type: object + properties: + name: + type: string + address: + type: object + properties: + street: + type: string + city: + type: string + items: + type: integer diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/localReferenceToJsonSchemaResource.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/localReferenceToJsonSchemaResource.yaml new file mode 100644 index 000000000..ce29f718f --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/localReferenceToJsonSchemaResource.yaml @@ -0,0 +1,23 @@ +openapi: 3.2.0 +info: + title: OpenAPI document containing examples for reuse + version: 1.0.0 +components: + schemas: + a: + type: + - object + - 'null' + properties: + b: + type: + - object + - 'null' + properties: + c: + type: + - object + - 'null' + properties: + b: + $ref: '#/properties/b' diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/recursiveRelativeSubschemaReference.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/recursiveRelativeSubschemaReference.json new file mode 100644 index 000000000..d58d97ebf --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/recursiveRelativeSubschemaReference.json @@ -0,0 +1,96 @@ +{ + "openapi": "3.2.0", + "info": { + "title": "Recursive relative reference in a subschema of an component schema", + "version": "1.0.0" + }, + "paths": { + "/items": { + "get": { + "responses": { + "200": { + "description": "ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Foo" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Foo": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "format": null, + "x-schema-id": null + }, + "parent": { + "type": [ + "object", + "null" + ], + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "format": null, + "x-schema-id": null + }, + "parent": { + "$ref": "#/properties/parent", + "x-schema-id": "Category" + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "format": null, + "x-schema-id": null + } + }, + "required": [ + "name" + ], + "x-schema-id": "Tag" + } + } + }, + "required": [ + "name" + ], + "x-schema-id": "Category" + }, + "tags": { + "$ref": "#/properties/parent/properties/tags" + } + }, + "required": [ + "name" + ], + "x-schema-id": "Category" + } + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/relativeSubschemaReference.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/relativeSubschemaReference.json new file mode 100644 index 000000000..cfeace1be --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/relativeSubschemaReference.json @@ -0,0 +1,58 @@ +{ + "openapi": "3.2.0", + "info": { + "title": "Relative reference in a subschema of an component schema", + "version": "1.0.0" + }, + "paths": { + "/items": { + "get": { + "responses": { + "200": { + "description": "ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Foo" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Foo": { + "type": "object", + "properties": { + "seq1": { + "type": [ + "array", + "null" + ], + "items": { + "type": "array", + "items": { + "type": "string", + "format": null, + "x-schema-id": null + } + } + }, + "seq2": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/properties/seq1/items" + } + } + }, + "x-schema-id": "ContainerType" + } + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/rootComponentSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/rootComponentSchemaReference.yaml new file mode 100644 index 000000000..a893382e0 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/rootComponentSchemaReference.yaml @@ -0,0 +1,23 @@ +openapi: 3.2.0 +info: + title: Reference at the root of a component schema + version: 1.0.0 +paths: + /items: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/specialitem' +components: + schemas: + specialitem: + $ref: "#/components/schemas/item" + item: + title: Item + type: object diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/rootInlineSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/rootInlineSchemaReference.yaml new file mode 100644 index 000000000..5374b1851 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/rootInlineSchemaReference.yaml @@ -0,0 +1,18 @@ +openapi: 3.2.0 +info: + title: Reference in at the root of an inline schema + version: 1.0.0 +paths: + /item: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/item' +components: + schemas: + item: + type: object diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/subschemaComponentSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/subschemaComponentSchemaReference.yaml new file mode 100644 index 000000000..2d480704e --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/subschemaComponentSchemaReference.yaml @@ -0,0 +1,22 @@ +openapi: 3.2.0 +info: + title: Reference in a subschema of an component schema + version: 1.0.0 +paths: + /items: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/items' +components: + schemas: + items: + type: array + items: + $ref: '#/components/schemas/item' + item: + type: object diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/subschemaInlineSchemaReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/subschemaInlineSchemaReference.yaml new file mode 100644 index 000000000..ce48ac2a1 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/ReferenceSamples/subschemaInlineSchemaReference.yaml @@ -0,0 +1,20 @@ +openapi: 3.2.0 +info: + title: Reference in at the root of an inline schema + version: 1.0.0 +paths: + /items: + get: + responses: + 200: + description: ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/item' +components: + schemas: + item: + type: object diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/RelativeReferenceTests.cs new file mode 100644 index 000000000..c2b335ec8 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/RelativeReferenceTests.cs @@ -0,0 +1,515 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.OpenApi.Reader; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests +{ + public class RelativeReferenceTests + { + private const string SampleFolderPath = "V32Tests/ReferenceSamples"; + + [Fact] + public async Task ParseInlineLocalReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "inlineLocalReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schemaType = actual.Paths["/item"].Operations[HttpMethod.Get].Parameters[0].Schema.Type; + + // Assert + Assert.Equal(JsonSchemaType.Number, schemaType); + } + + [Fact] + public async Task ParseInlineExternalReferenceWorks() + { + // Arrange + var expected = new JsonArray + { + new JsonObject + { + ["name"] = "thing", + ["description"] = "a thing" + } + }; + + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "inlineExternalReference.yaml"), settings)).Document; + var exampleValue = actual.Paths["/items"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Examples["item-list"].Value; + + // Assert + Assert.NotNull(exampleValue); + Assert.IsType(exampleValue); + Assert.Equal(expected.ToJsonString(), exampleValue.ToJsonString()); + } + + [Fact] + public async Task ParseComponentExternalReferenceWorks() + { + // Arrange + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "componentExternalReference.yaml"), settings)).Document; + var securitySchemeValue = actual.Components.SecuritySchemes["customapikey"]; + + // Assert + Assert.Equal("x-api-key", securitySchemeValue.Name); + } + + [Fact] + public async Task ParseRootInlineJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "rootInlineSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Paths["/item"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseSubschemaInlineJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "subschemaInlineSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Paths["/items"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema.Items; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseRootComponentJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "rootComponentSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Components.Schemas["specialitem"]; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + Assert.Equal("Item", schema.Title); + } + + [Fact] + public async Task ParseSubschemaComponentJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "subschemaComponentSchemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Components.Schemas["items"].Items; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseInternalComponentSubschemaJsonSchemaReferenceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "internalComponentsSubschemaReference.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var addressSchema = actual.Paths["/person/{id}/address"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + var itemsSchema = actual.Paths["/human"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, addressSchema.Type); + Assert.Equal(JsonSchemaType.Integer, itemsSchema.Type); + } + + [Fact] + public async Task ParseExternalComponentSubschemaJsonSchemaReferenceWorks() + { + // Arrange + var path = Path.Combine(Directory.GetCurrentDirectory(), SampleFolderPath); + var settings = new OpenApiReaderSettings + { + LoadExternalRefs = true, + BaseUrl = new(path), + }; + settings.AddYamlReader(); + + // Act + var actual = (await OpenApiDocument.LoadAsync(Path.Combine(SampleFolderPath, "externalComponentSubschemaReference.yaml"), settings)).Document; + var schema = actual.Paths["/person/{id}"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseReferenceToInternalComponentUsingDollarIdWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "internalComponentReferenceUsingId.yaml"); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Paths["/person/{id}"].Operations[HttpMethod.Get].Responses["200"].Content["application/json"].Schema; + + // Assert + Assert.Equal(JsonSchemaType.Object, schema.Type); + } + + [Fact] + public async Task ParseLocalReferenceToJsonSchemaResourceWorks() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "localReferenceToJsonSchemaResource.yaml"); + var stringWriter = new StringWriter(); + var writer = new OpenApiYamlWriter(stringWriter); + + // Act + var actual = (await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings)).Document; + var schema = actual.Components.Schemas["a"].Properties["b"].Properties["c"].Properties["b"]; + schema.SerializeAsV32(writer); + + // Assert + Assert.Equal(JsonSchemaType.Object | JsonSchemaType.Null, schema.Type); + } + + [Fact] + public void ResolveSubSchema_ShouldTraverseKnownKeywords() + { + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["a"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["b"] = new OpenApiSchema { Type = JsonSchemaType.String } + } + } + } + }; + + var path = new[] { "properties", "a", "properties", "b" }; + + var result = OpenApiWorkspace.ResolveSubSchema(schema, path, []); + + Assert.NotNull(result); + Assert.Equal(JsonSchemaType.String, result!.Type); + } + + public static IEnumerable SubSchemaKeywordPropertyPaths => + [ + [new[] { "properties", "properties" }], + [new[] { "properties", "allOf" }] + ]; + + + [Theory] + [MemberData(nameof(SubSchemaKeywordPropertyPaths))] + public void ResolveSubSchema_ShouldHandleUserDefinedKeywordNamedProperty(string[] pathSegments) + { + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["properties"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["allOf"] = new OpenApiSchema { Type = JsonSchemaType.String } + } + }; + + var result = OpenApiWorkspace.ResolveSubSchema(schema, pathSegments, []); + + Assert.NotNull(result); + Assert.Equal(JsonSchemaType.String, result!.Type); + } + + [Fact] + public void ResolveSubSchema_ShouldRecurseIntoAllOfComposition() + { + var schema = new OpenApiSchema + { + AllOf = + [ + new OpenApiSchema + { + Properties = new Dictionary + { + ["x"] = new OpenApiSchema { Type = JsonSchemaType.Integer } + } + } + ] + }; + + var path = new[] { "allOf", "0", "properties", "x" }; + + var result = OpenApiWorkspace.ResolveSubSchema(schema, path, []); + + Assert.NotNull(result); + Assert.Equal(JsonSchemaType.Integer, result!.Type); + } + [Fact] + public async Task ShouldResolveRelativeSubReference() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "relativeSubschemaReference.json"); + + // Act + var (actual, _) = await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings); + + var fooComponentSchema = actual.Components.Schemas["Foo"]; + var seq1Property = fooComponentSchema.Properties["seq1"]; + Assert.NotNull(seq1Property); + var seq2Property = fooComponentSchema.Properties["seq2"]; + Assert.NotNull(seq2Property); + Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type); + Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type); + } + [Fact] + public async Task ShouldResolveRelativeSubReferenceUsingParsingContext() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "relativeSubschemaReference.json"); + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); + var jsonNode = await JsonNode.ParseAsync(fs); + var schemaJsonNode = jsonNode["components"]?["schemas"]?["Foo"]; + Assert.NotNull(schemaJsonNode); + var diagnostic = new OpenApiDiagnostic(); + var parsingContext = new ParsingContext(diagnostic); + parsingContext.StartObject("components"); + parsingContext.StartObject("schemas"); + parsingContext.StartObject("Foo"); + var document = new OpenApiDocument(); + + // Act + var fooComponentSchema = parsingContext.ParseFragment(schemaJsonNode, OpenApiSpecVersion.OpenApi3_2, document); + document.AddComponent("Foo", fooComponentSchema); + var seq1Property = fooComponentSchema.Properties["seq1"]; + Assert.NotNull(seq1Property); + var seq2Property = fooComponentSchema.Properties["seq2"]; + Assert.NotNull(seq2Property); + Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type); + Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type); + } + [Fact] + public void ShouldFailToResolveRelativeSubReferenceFromTheObjectModel() + { + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" }, + }; + document.Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["Foo"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["seq1"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } }, + ["seq2"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchemaReference("#/properties/seq1/items", document) } + } + } + } + }; + document.RegisterComponents(); + + var fooComponentSchema = document.Components.Schemas["Foo"]; + var seq1Property = fooComponentSchema.Properties["seq1"]; + Assert.NotNull(seq1Property); + var seq2Property = fooComponentSchema.Properties["seq2"]; + Assert.NotNull(seq2Property); + Assert.Throws(() => seq2Property.Items.Type); + // it's impossible to resolve relative references from the object model only because we don't have a way to get to + // the parent object to build the full path for the reference. + + + // #/properties/seq1/items + // #/components/schemas/Foo/properties/seq1/items + } + [Fact] + public void ShouldResolveAbsoluteSubReferenceFromTheObjectModel() + { + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" }, + }; + document.Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["Foo"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["seq1"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } }, + ["seq2"] = new OpenApiSchema { Type = JsonSchemaType.Array | JsonSchemaType.Null, Items = new OpenApiSchemaReference("#/components/schemas/Foo/properties/seq1/items", document) } + } + } + } + }; + document.RegisterComponents(); + + var fooComponentSchema = document.Components.Schemas["Foo"]; + var seq1Property = fooComponentSchema.Properties["seq1"]; + Assert.NotNull(seq1Property); + var seq2Property = fooComponentSchema.Properties["seq2"]; + Assert.NotNull(seq2Property); + Assert.Equal(JsonSchemaType.Array, seq2Property.Items.Type); + Assert.Equal(JsonSchemaType.String, seq2Property.Items.Items.Type); + } + [Fact] + public async Task ShouldResolveRecursiveRelativeSubReference() + { + // Arrange + var filePath = Path.Combine(SampleFolderPath, "recursiveRelativeSubschemaReference.json"); + + // Act + var (actual, _) = await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings); + + var fooComponentSchema = actual.Components.Schemas["Foo"]; + var fooSchemaParentProperty = fooComponentSchema.Properties["parent"]; + Assert.NotNull(fooSchemaParentProperty); + var fooSchemaParentPropertyTagsProperty = fooSchemaParentProperty.Properties["tags"]; + Assert.NotNull(fooSchemaParentPropertyTagsProperty); + Assert.Equal(JsonSchemaType.Array | JsonSchemaType.Null, fooSchemaParentPropertyTagsProperty.Type); + Assert.Equal(JsonSchemaType.Object, fooSchemaParentPropertyTagsProperty.Items.Type); + + var fooSchemaTagsProperty = fooComponentSchema.Properties["tags"]; + Assert.NotNull(fooSchemaTagsProperty); + Assert.Equal(JsonSchemaType.Array | JsonSchemaType.Null, fooSchemaTagsProperty.Type); + Assert.Equal(JsonSchemaType.Object, fooSchemaTagsProperty.Items.Type); + } + [Fact] + public async Task ShouldResolveReferencesInSchemasFromSystemTextJson() + { + var filePath = Path.Combine(SampleFolderPath, "STJSchema.json"); + using var fs = File.OpenRead(filePath); + var jsonNode = await JsonNode.ParseAsync(fs); + + var parsingContext = new ParsingContext(new OpenApiDiagnostic()); + var document = new OpenApiDocument(); + var schema = parsingContext.ParseFragment(jsonNode, OpenApiSpecVersion.OpenApi3_2, document); + Assert.NotNull(schema); + + document.AddComponent("Foo", schema); + var tagsProperty = Assert.IsType(schema.Properties["tags"]); + // this is the reference that is generated by STJ schema generator which does not have OAI in context. + Assert.Equal("#/properties/parent/properties/tags", tagsProperty.Reference.ReferenceV3); + // this is the reference that needs to be used in the document for components resolution. + var absoluteReferenceId = $"#/components/schemas/Foo{tagsProperty.Reference.ReferenceV3.Replace("#", string.Empty)}"; + schema.Properties["tags"] = new OpenApiSchemaReference(absoluteReferenceId, document); + var updatedTagsProperty = Assert.IsType(schema.Properties["tags"]); + Assert.Equal(absoluteReferenceId, updatedTagsProperty.Reference.ReferenceV3); + Assert.Equal(JsonSchemaType.Array | JsonSchemaType.Null, updatedTagsProperty.Type); + Assert.Equal(JsonSchemaType.Object, updatedTagsProperty.Items.Type); + + + // doing the same for the parent property + + var parentProperty = Assert.IsType(schema.Properties["parent"]); + var parentSubProperty = Assert.IsType(parentProperty.Properties["parent"]); + Assert.Equal("#/properties/parent", parentSubProperty.Reference.ReferenceV3); + parentProperty.Properties["parent"] = new OpenApiSchemaReference($"#/components/schemas/Foo{parentSubProperty.Reference.ReferenceV3.Replace("#", string.Empty)}", document); + var updatedParentSubProperty = Assert.IsType(parentProperty.Properties["parent"]); + Assert.Equal(JsonSchemaType.Object | JsonSchemaType.Null, updatedParentSubProperty.Type); + + var pathItem = new OpenApiPathItem + { + Operations = new Dictionary + { + [HttpMethod.Post] = new OpenApiOperation + { + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + } + }, + RequestBody = new OpenApiRequestBody + { + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference("#/components/schemas/Foo", document) + } + } + } + } + } + }; + document.Paths.Add("/", pathItem); + + var requestBodySchema = pathItem.Operations[HttpMethod.Post].RequestBody.Content["application/json"].Schema; + Assert.NotNull(requestBodySchema); + var requestBodyTagsProperty = Assert.IsType(requestBodySchema.Properties["tags"]); + Assert.Equal(JsonSchemaType.Object, requestBodyTagsProperty.Items.Type); + } + + [Fact] + public void ExitsEarlyOnCyclicalReferences() + { + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" }, + }; + var categorySchema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["name"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["parent"] = new OpenApiSchemaReference("#/components/schemas/Category", document), + // this is intentionally wrong and cyclical reference + // it tests whether we're going in an infinite resolution loop + ["tags"] = new OpenApiSchemaReference("#/components/schemas/Category/properties/parent/properties/tags", document) + } + }; + document.AddComponent("Category", categorySchema); + document.RegisterComponents(); + + var tagsSchemaRef = Assert.IsType(categorySchema.Properties["tags"]); + Assert.Throws(() => tagsSchemaRef.Items); + Assert.Equal("#/components/schemas/Category/properties/parent/properties/tags", tagsSchemaRef.Reference.ReferenceV3); + Assert.Throws(() => tagsSchemaRef.Target); + + var parentSchemaRef = Assert.IsType(categorySchema.Properties["parent"]); + Assert.Equal("#/components/schemas/Category", parentSchemaRef.Reference.ReferenceV3); + Assert.NotNull(parentSchemaRef.Target); + } + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWith32properties.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWith32properties.json new file mode 100644 index 000000000..e98c74362 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWith32properties.json @@ -0,0 +1,166 @@ +{ + "openapi": "3.2.0", + "info": { + "title": "Sample OpenAPI 3.2 API", + "description": "A sample API demonstrating OpenAPI 3.2 features", + "version": "2.0.0", + "summary": "Sample OpenAPI 3.2 API with the latest features", + "license": { + "name": "Apache 2.0", + "identifier": "Apache-2.0" + } + }, + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "servers": [ + { + "url": "https://api.example.com/v2", + "description": "Main production server" + } + ], + "webhooks": { + "newPetAlert": { + "post": { + "summary": "Notify about a new pet being added", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "Webhook processed successfully" + } + } + } + } + }, + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "schema": { + "type": "integer", + "exclusiveMinimum": 1, + "exclusiveMaximum": 100 + } + } + ], + "responses": { + "200": { + "description": "A paged array of pets", + "content": { + "application/json": { + "schema": { + "$ref": "https://example.com/schemas/pet.json" + } + } + } + } + } + } + }, + "/sample": { + "get": { + "summary": "Sample endpoint", + "responses": { + "200": { + "description": "Sample response", + "content": { + "application/json": { + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/schemas/person.schema.yaml", + "$comment": "A schema defining a pet object with optional references to dynamic components.", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": false, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": false + }, + "title": "Pet", + "description": "Schema for a pet object", + "type": "object", + "properties": { + "name": { + "type": "string", + "$comment": "The pet's full name" + }, + "address": { + "$dynamicRef": "#addressDef", + "$comment": "Reference to an address definition which can change dynamically" + } + }, + "required": [ + "name" + ], + "$dynamicAnchor": "addressDef" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }, + "schemas": { + "Pet": { + "$id": "https://example.com/schemas/pet.json", + "type": "object", + "required": [ + "id", + "weight" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "weight": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Weight of the pet in kilograms" + }, + "attributes": { + "type": [ + "object", + "null" + ], + "description": "Dynamic attributes for the pet", + "patternProperties": { + "^attr_[A-Za-z]+$": { + "type": "string" + } + } + } + }, + "$comment": "This schema represents a pet in the system.", + "$defs": { + "ExtraInfo": { + "type": "string" + } + } + } + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithExample.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithExample.yaml new file mode 100644 index 000000000..85560384a --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithExample.yaml @@ -0,0 +1,96 @@ +openapi: 3.2.0 # The version of the OpenAPI Specification +info: # Metadata about the API + title: A simple OpenAPI 3.2 example + version: 1.0.0 + license: + name: Apache 2.0 + identifier: Apache-2.0 # The SPDX license identifier +paths: # The available paths and operations for the API + /echo: # A path for echoing messages using WebSockets + get: # An operation using the GET method + summary: Echo a message + description: Send a message to the server and receive the same message back + responses: + '101': + description: Switching Protocols + headers: + Upgrade: + schema: + type: string + enum: + - websocket + Connection: + schema: + type: string + enum: + - Upgrade + Sec-WebSocket-Accept: + schema: + type: string + content: {} # No content is returned for this response + servers: + - url: ws://example.com # The WebSocket server URL + /upload: # A path for uploading files using multipart/form-data + post: # An operation using the POST method + summary: Upload a file + description: Upload a file to the server and receive a confirmation message + requestBody: + required: true + content: + multipart/form-data: # The media type for sending multiple parts of data + schema: + type: object + properties: + file: # A property for the file data + type: string + format: binary + comment: # A property for the file comment + type: string + encoding: # The encoding for each part of data + file: + contentType: application/octet-stream # The media type for the file data + comment: + contentType: text/plain # The media type for the file comment + responses: + '200': + description: File uploaded successfully + content: + application/json: # The media type for the response body + schema: + type: object + properties: + message: # A property for the confirmation message + type: string + examples: + - The file was uploaded successfully +components: # Reusable components for the API + schemas: # JSON Schema definitions for the API + Pet: # A schema for a pet object + type: object + required: + - petType + properties: + petType: # A property for the pet type + type: string + discriminator: # The discriminator for resolving the concrete schema type + propertyName: petType + mapping: + cat: '#/components/schemas/Cat' + dog: '#/components/schemas/Dog' + Cat: # A schema for a cat object + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + name: # A property for the cat name + type: string + default: "Fluffy" # The default value for the cat name + Dog: # A schema for a dog object + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + bark: # A property for the dog bark + type: string + default: "Woof" # The default value for the dog bark + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithPatternPropertiesInSchema.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithPatternPropertiesInSchema.yaml new file mode 100644 index 000000000..b51e48523 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithPatternPropertiesInSchema.yaml @@ -0,0 +1,26 @@ +openapi: 3.2.0 +info: + title: Example API + version: 1.0.0 +paths: + /example: + get: + summary: Get example object + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + prop1: + type: string + prop2: + type: string + prop3: + type: string + patternProperties: + "^x-.*$": + type: string + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithReferenceById.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithReferenceById.yaml new file mode 100644 index 000000000..26c0ae066 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithReferenceById.yaml @@ -0,0 +1,45 @@ +openapi: 3.2.0 +info: + title: ReferenceById + version: 1.0.0 +paths: + /resource: + get: + parameters: + - name: id + in: query + required: true + schema: + $ref: 'https://example.com/schemas/id.json' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: 'https://example.com/schemas/resource.json' + post: + requestBody: + required: true + content: + application/json: + schema: + $ref: 'https://example.com/schemas/resource.json' + responses: + '200': + description: OK +components: + schemas: + Resource: + $id: 'https://example.com/schemas/resource.json' + type: object + properties: + id: + type: string + name: + type: string + reference: + $ref: '#/components/schemas/Resource' + Id: + $id: 'https://example.com/schemas/id.json' + type: string diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithReferencedExampleInSchemaWorks.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithReferencedExampleInSchemaWorks.yaml new file mode 100644 index 000000000..0d8163cd5 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/docWithReferencedExampleInSchemaWorks.yaml @@ -0,0 +1,30 @@ +openapi: 3.2.0 +info: + title: ReferenceById + version: 1.0.0 +paths: + /resource: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DiffCreatedEvent' +components: + schemas: + DiffCreatedEvent: + description: 'diff index created' + type: object + additionalProperties: false + properties: + updatedAt: + $ref: '#/components/schemas/Timestamp' + example: + "updatedAt": '2020-06-30T06:43:51.391Z' + Timestamp: + type: string + format: date-time + description: 'timestamp' + example: '2020-06-30T06:43:51.391Z' diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWith32Properties.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWith32Properties.yaml new file mode 100644 index 000000000..d7262b59d --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWith32Properties.yaml @@ -0,0 +1,129 @@ +openapi: 3.2.0 +info: + title: Sample OpenAPI 3.2 API + description: A sample API demonstrating OpenAPI 3.2 features + version: "2.0.0" + summary: Sample OpenAPI 3.2 API with the latest features # OpenAPI 3.2 feature + license: + name: Apache 2.0 + identifier: Apache-2.0 # SPDX license identifier, a new 3.2 feature to define an API's SPDX license expression + +# JSON Schema 2020-12 feature +jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema" + +servers: + - url: https://api.example.com/v2 + description: Main production server + +# Example Webhooks (OpenAPI 3.2 feature) +webhooks: + newPetAlert: + post: + summary: Notify about a new pet being added + requestBody: + required: true + content: + application/json: + schema: + type: string + responses: + '200': + description: Webhook processed successfully +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + #exclusiveMinimum and exclusiveMaximum now represent distinct values + exclusiveMinimum: 1 + exclusiveMaximum: 100 + responses: + '200': + description: A paged array of pets + content: + application/json: + schema: + # 3.2 feature where we can reference schemas using their identifier + $ref: 'https://example.com/schemas/pet.json' + /sample: + get: + summary: Sample endpoint + responses: + '200': + description: Sample response + content: + application/json: + schema: + #JSON schema keywords + $schema: "https://json-schema.org/draft/2020-12/schema" + $id: "https://example.com/schemas/person.schema.yaml" + $comment: "A schema defining a pet object with optional references to dynamic components." + $vocabulary: + "https://json-schema.org/draft/2020-12/vocab/core": true + "https://json-schema.org/draft/2020-12/vocab/applicator": true + "https://json-schema.org/draft/2020-12/vocab/validation": true + "https://json-schema.org/draft/2020-12/vocab/meta-data": false + "https://json-schema.org/draft/2020-12/vocab/format-annotation": false + + title: "Pet" + description: "Schema for a pet object" + type: "object" + properties: + name: + type: "string" + $comment: "The pet's full name" + address: + $dynamicRef: "#addressDef" + $comment: "Reference to an address definition which can change dynamically" + required: + - name + $dynamicAnchor: "addressDef" +components: + securitySchemes: + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + $id: 'https://example.com/schemas/pet.json' + type: object + required: + - id + - weight + properties: + id: + type: string + format: uuid + weight: + type: number + exclusiveMinimum: 0 + description: Weight of the pet in kilograms + # Pattern properties and Type array feature from JSON Schema + attributes: + type: + - "object" + - "null" + description: Dynamic attributes for the pet + patternProperties: + "^attr_[A-Za-z]+$": + type: string + $comment: "This schema represents a pet in the system." # JSON Schema 2020-12 feature + $defs: # JSON Schema 2020-12 feature + ExtraInfo: + type: string + +tags: + - name: pets + +security: + - api_key: [] diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithEmptyTags.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithEmptyTags.json new file mode 100644 index 000000000..00834158e --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithEmptyTags.json @@ -0,0 +1,51 @@ +{ + "openapi": "3.2.0", + "info": { + "description": "Groups API", + "title": "Groups", + "version": "1.0" + }, + "paths": { + "/groups": { + "get": { + "operationId": "getGroups", + "parameters": [ + { + "description": "Zero-based page index (0..N)", + "example": 0, + "in": "query", + "name": "page", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 + } + } + ], + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedGroup" } } } + } + }, + "tags": [ "" ] + } + } + }, + "components": { + "schemas": { + "PaginatedGroup": { + "type": "object", + "properties": { + "number": { + "type": "integer", + "format": "int32", + "description": "The number of the current page." + } + } + } + } + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithReusablePaths.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithReusablePaths.yaml new file mode 100644 index 000000000..cfa3872ee --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithReusablePaths.yaml @@ -0,0 +1,95 @@ +openapi : 3.2.0 +info: + title: Webhook Example + version: 1.0.0 +jsonSchemaDialect: "http://json-schema.org/draft-07/schema#" +webhooks: + pets: + "$ref": '#/components/pathItems/pets' +components: + schemas: + petSchema: + type: object + required: + - id + - name + dependentRequired: + tag: + - category + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + category: + type: string + newPetSchema: + type: object + required: + - name + dependentRequired: + tag: + - category + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + category: + type: string + pathItems: + pets: + get: + description: Returns all pets from the system that the user has access to + operationId: findPets + parameters: + - name: tags + in: query + description: tags to filter by + required: false + schema: + type: array + items: + type: string + - name: limit + in: query + description: maximum number of results to return + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: pet response + content: + application/json: + schema: + type: array + items: + "$ref": '#/components/schemas/petSchema' + application/xml: + schema: + type: array + items: + "$ref": '#/components/schemas/petSchema' + post: + requestBody: + description: Information about a new pet in the system + required: true + content: + 'application/json': + schema: + "$ref": '#/components/schemas/newPetSchema' + responses: + "200": + description: Return a 200 status to indicate that the data was received successfully + content: + application/json: + schema: + $ref: '#/components/schemas/petSchema' diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithSchema.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithSchema.json new file mode 100644 index 000000000..51427b914 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithSchema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://spec.openapis.org/oas/3.2/schema/2025-02-13", + "openapi": "3.2.0", + "info": { + "title": "Sample API", + "version": "1.0.0" + }, + "paths": { + "/example": { + "get": { + "summary": "Example operation", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithSummaryAndDescriptionInReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithSummaryAndDescriptionInReference.yaml new file mode 100644 index 000000000..87d44d28a --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithSummaryAndDescriptionInReference.yaml @@ -0,0 +1,43 @@ +openapi: '3.2.0' +info: + version: '1.0.0' + title: Swagger Petstore (Simple) +paths: + /pets: + get: + description: Returns all pets from the system that the user has access to + responses: + '200': + description: pet response + content: + application/json: + schema: + "$ref": '#/components/schemas/pet' +components: + headers: + X-Test: + description: Test + summary: An X-Test header + schema: + type: string + responses: + Test: + description: Test Response + headers: + X-Test: + $ref: '#/components/headers/X-Test' + schemas: + pet: + description: A referenced pet in a petstore + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml new file mode 100644 index 000000000..08bbfff80 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml @@ -0,0 +1,91 @@ +openapi: 3.2.0 +info: + title: Webhook Example + version: 1.0.0 +webhooks: + pets: + get: + description: Returns all pets from the system that the user has access to + operationId: findPets + parameters: + - name: tags + in: query + description: tags to filter by + required: false + schema: + type: array + items: + type: string + - name: limit + in: query + description: maximum number of results to return + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: pet response + content: + application/json: + schema: + type: array + items: + "$ref": '#/components/schemas/petSchema' + application/xml: + schema: + type: array + items: + "$ref": '#/components/schemas/petSchema' + post: + requestBody: + description: Information about a new pet in the system + required: true + content: + 'application/json': + schema: + "$ref": '#/components/schemas/newPetSchema' + responses: + "200": + description: Return a 200 status to indicate that the data was received successfully + content: + application/json: + schema: + $ref: '#/components/schemas/petSchema' +components: + schemas: + petSchema: + type: object + required: + - id + - name + dependentRequired: + tag: + - category + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + category: + type: string + newPetSchema: + type: object + required: + - name + dependentRequired: + tag: + - category + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + category: + type: string diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/externalRefById.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/externalRefById.yaml new file mode 100644 index 000000000..68f5ffdfd --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/externalRefById.yaml @@ -0,0 +1,14 @@ +openapi: 3.2.0 +info: + title: ReferenceById + version: 1.0.0 +paths: + /resource: + get: + parameters: + - name: id + in: query + required: true + schema: + $ref: 'https://example.com/schemas/user.json' +components: {} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/externalRefByJsonPointer.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/externalRefByJsonPointer.yaml new file mode 100644 index 000000000..25c18f845 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/externalRefByJsonPointer.yaml @@ -0,0 +1,15 @@ +openapi: 3.2.0 +info: + title: ReferenceById + version: 1.0.0 +paths: + /resource: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './externalResource.yaml#/components/schemas/todo' +components: {} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/externalResource.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/externalResource.yaml new file mode 100644 index 000000000..ce0efebd7 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiDocument/externalResource.yaml @@ -0,0 +1,22 @@ +openapi: 3.2.0 +info: + title: ReferencedById + version: 1.0.0 +paths: {} +components: + schemas: + todo: + type: object + properties: + id: + type: string + name: + type: string + user: + $id: 'https://example.com/schemas/user.json' + type: object + properties: + id: + type: string + name: + type: string diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiInfo/basicInfo.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiInfo/basicInfo.yaml new file mode 100644 index 000000000..941f6dbd3 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiInfo/basicInfo.yaml @@ -0,0 +1,17 @@ +{ + "title": "Basic Info", + "summary": "Sample Summary", + "description": "Sample Description", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.1" +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiLicense/licenseWithSpdxIdentifier.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiLicense/licenseWithSpdxIdentifier.yaml new file mode 100644 index 000000000..7a0d3bd76 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiLicense/licenseWithSpdxIdentifier.yaml @@ -0,0 +1,3 @@ +name: Apache 2.0 +identifier: Apache-2.0 + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/advancedSchema.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/advancedSchema.yaml new file mode 100644 index 000000000..a01d9f294 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/advancedSchema.yaml @@ -0,0 +1,48 @@ +type: object +properties: + one: + description: type array + type: + - integer + - string + two: + description: type 'null' + type: "null" + three: + description: type array including 'null' + type: + - string + - "null" + four: + description: array with no items + type: array + five: + description: singular example + type: string + examples: + - exampleValue + six: + description: exclusiveMinimum true + exclusiveMinimum: 10 + seven: + description: exclusiveMinimum false + minimum: 10 + eight: + description: exclusiveMaximum true + exclusiveMaximum: 20 + nine: + description: exclusiveMaximum false + maximum: 20 + ten: + description: nullable string + type: + - string + - "null" + eleven: + description: x-nullable string + type: + - string + - "null" + twelve: + description: file/binary + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/jsonSchema.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/jsonSchema.json new file mode 100644 index 000000000..dc55b72c2 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/jsonSchema.json @@ -0,0 +1,49 @@ +{ + "$id": "https://example.com/arrays.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "A representation of a person, company, organization, or place", + "type": "object", + "properties": { + "fruits": { + "type": "array", + "items": { + "type": "string" + } + }, + "vegetables": { + "type": "array" + } + }, + "$defs": { + "veggie": { + "type": "object", + "required": [ "veggieName", "veggieLike" ], + "properties": { + "veggieName": { + "type": "string", + "description": "The name of the vegetable." + }, + "veggieLike": { + "type": "boolean", + "description": "Do I like this vegetable?" + }, + "veggieType": { + "type": "string", + "description": "The type of vegetable (e.g., root, leafy, etc.)." + }, + "veggieColor": { + "type": "string", + "description": "The color of the vegetable." + }, + "veggieSize": { + "type": "string", + "description": "The size of the vegetable." + } + }, + "dependentRequired": { + "veggieType": [ "veggieColor", "veggieSize" ] + } + } + } +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schema.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schema.yaml new file mode 100644 index 000000000..0b2740b28 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schema.yaml @@ -0,0 +1,8 @@ +type: object +properties: + one: + description: type array + type: + - integer + - string + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithConst.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithConst.json new file mode 100644 index 000000000..d394f142f --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithConst.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "active" + }, + "user": { + "type": "object", + "properties": { + "role": { + "type": "string", + "const": "admin" + } + }, + "required": [ "role" ] + } + }, + "required": [ "status" ] +} + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithExamples.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithExamples.yaml new file mode 100644 index 000000000..98f19e3f0 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithExamples.yaml @@ -0,0 +1,4 @@ +type: string +examples: + - fedora + - ubuntu diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml new file mode 100644 index 000000000..0c20b0d3d --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml @@ -0,0 +1,31 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "https://example.com/schemas/person.schema.yaml" +$comment: "A schema defining a person object with optional references to dynamic components." +$vocabulary: + "https://json-schema.org/draft/2020-12/vocab/core": true + "https://json-schema.org/draft/2020-12/vocab/applicator": true + "https://json-schema.org/draft/2020-12/vocab/validation": true + "https://json-schema.org/draft/2020-12/vocab/meta-data": false + "https://json-schema.org/draft/2020-12/vocab/format-annotation": false + +title: "Person" +description: "Schema for a person object" +type: "object" + +properties: + name: + type: "string" + $comment: "The person's full name" + age: + type: "integer" + minimum: 0 + $comment: "Age must be a non-negative integer" + address: + $dynamicRef: "#addressDef" + $comment: "Reference to an address definition which can change dynamically" + +required: + - name + +$dynamicAnchor: "addressDef" + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithNullable.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithNullable.yaml new file mode 100644 index 000000000..5953df083 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithNullable.yaml @@ -0,0 +1,2 @@ +type: string +nullable: true diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithNullableExtension.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithNullableExtension.yaml new file mode 100644 index 000000000..271d5f4c9 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithNullableExtension.yaml @@ -0,0 +1,2 @@ +type: string +x-nullable: true diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithTypeArray.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithTypeArray.yaml new file mode 100644 index 000000000..2a916fb56 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/schemaWithTypeArray.yaml @@ -0,0 +1,3 @@ +type: +- "string" +- "null"