From afa0b365a9bc4a45e3b245903ce3eacfe2658d40 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 3 Jun 2024 16:41:45 -0700 Subject: [PATCH] Add test coverage for generic types (#55940) --- .../src/Schemas/OpenApiJsonSchema.Helpers.cs | 8 ++ .../src/Schemas/OpenApiSchemaKeywords.cs | 1 + ...OpenApiComponentService.ResponseSchemas.cs | 136 ++++++++++++++++++ src/OpenApi/test/SharedTypes.cs | 9 ++ 4 files changed, 154 insertions(+) diff --git a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs index e76fdc8811dd..7493f4313732 100644 --- a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs +++ b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs @@ -265,6 +265,11 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName, var props = ReadDictionary(ref reader); schema.Properties = props?.ToDictionary(p => p.Key, p => p.Value.Schema); break; + case OpenApiSchemaKeywords.AdditionalPropertiesKeyword: + reader.Read(); + var additionalPropsConverter = (JsonConverter)options.GetTypeInfo(typeof(OpenApiJsonSchema)).Converter; + schema.AdditionalProperties = additionalPropsConverter.Read(ref reader, typeof(OpenApiJsonSchema), options)?.Schema; + break; case OpenApiSchemaKeywords.AnyOfKeyword: reader.Read(); schema.Type = "object"; @@ -284,6 +289,9 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName, var mappings = ReadDictionary(ref reader); schema.Discriminator.Mapping = mappings; break; + default: + reader.Skip(); + break; } } } diff --git a/src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs b/src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs index 063569ea270f..69ba96d77016 100644 --- a/src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs +++ b/src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs @@ -7,6 +7,7 @@ internal class OpenApiSchemaKeywords public const string FormatKeyword = "format"; public const string ItemsKeyword = "items"; public const string PropertiesKeyword = "properties"; + public const string AdditionalPropertiesKeyword = "additionalProperties"; public const string RequiredKeyword = "required"; public const string AnyOfKeyword = "anyOf"; public const string EnumKeyword = "enum"; diff --git a/src/OpenApi/test/Services/OpenApiComponentService/OpenApiComponentService.ResponseSchemas.cs b/src/OpenApi/test/Services/OpenApiComponentService/OpenApiComponentService.ResponseSchemas.cs index 4a1eb57f7162..c9edafae041d 100644 --- a/src/OpenApi/test/Services/OpenApiComponentService/OpenApiComponentService.ResponseSchemas.cs +++ b/src/OpenApi/test/Services/OpenApiComponentService/OpenApiComponentService.ResponseSchemas.cs @@ -393,4 +393,140 @@ await VerifyOpenApiDocument(builder, document => }); }); } + + [Fact] + public async Task GetOpenApiResponse_HandlesGenericType() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapGet("/", () => TypedResults.Ok>(new(0, 1, 5, 50, [new Todo(1, "Test Title", true, DateTime.Now), new Todo(2, "Test Title 2", false, DateTime.Now)]))); + + // Assert that the response schema is correctly generated. For now, generics are inlined + // in the generated OpenAPI schema since OpenAPI supports generics via dynamic references as of + // OpenAPI 3.1.0. + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/"].Operations[OperationType.Get]; + var responses = Assert.Single(operation.Responses); + var response = responses.Value; + Assert.True(response.Content.TryGetValue("application/json", out var mediaType)); + Assert.Equal("object", mediaType.Schema.Type); + Assert.Collection(mediaType.Schema.Properties, + property => + { + Assert.Equal("pageIndex", property.Key); + Assert.Equal("integer", property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }, + property => + { + Assert.Equal("pageSize", property.Key); + Assert.Equal("integer", property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }, + property => + { + Assert.Equal("totalItems", property.Key); + Assert.Equal("integer", property.Value.Type); + Assert.Equal("int64", property.Value.Format); + }, + property => + { + Assert.Equal("totalPages", property.Key); + Assert.Equal("integer", property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }, + property => + { + Assert.Equal("items", property.Key); + Assert.Equal("array", property.Value.Type); + Assert.NotNull(property.Value.Items); + Assert.Equal("object", property.Value.Items.Type); + Assert.Collection(property.Value.Items.Properties, + property => + { + Assert.Equal("id", property.Key); + Assert.Equal("integer", property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }, + property => + { + Assert.Equal("title", property.Key); + Assert.Equal("string", property.Value.Type); + }, + property => + { + Assert.Equal("completed", property.Key); + Assert.Equal("boolean", property.Value.Type); + }, + property => + { + Assert.Equal("createdAt", property.Key); + Assert.Equal("string", property.Value.Type); + Assert.Equal("date-time", property.Value.Format); + }); + }); + }); + } + + [Fact] + public async Task GetOpenApiResponse_HandlesValidationProblem() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapGet("/", () => TypedResults.ValidationProblem(new Dictionary + { + ["Name"] = ["Name is required"] + })); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/"].Operations[OperationType.Get]; + var responses = Assert.Single(operation.Responses); + var response = responses.Value; + Assert.True(response.Content.TryGetValue("application/problem+json", out var mediaType)); + Assert.Equal("object", mediaType.Schema.Type); + Assert.Collection(mediaType.Schema.Properties, + property => + { + Assert.Equal("type", property.Key); + Assert.Equal("string", property.Value.Type); + }, + property => + { + Assert.Equal("title", property.Key); + Assert.Equal("string", property.Value.Type); + }, + property => + { + Assert.Equal("status", property.Key); + Assert.Equal("integer", property.Value.Type); + Assert.Equal("int32", property.Value.Format); + }, + property => + { + Assert.Equal("detail", property.Key); + Assert.Equal("string", property.Value.Type); + }, + property => + { + Assert.Equal("instance", property.Key); + Assert.Equal("string", property.Value.Type); + }, + property => + { + Assert.Equal("errors", property.Key); + Assert.Equal("object", property.Value.Type); + // The errors object is a dictionary of string[]. Use `additionalProperties` + // to indicate that the payload can be arbitrary keys with string[] values. + Assert.Equal("array", property.Value.AdditionalProperties.Type); + Assert.Equal("string", property.Value.AdditionalProperties.Items.Type); + }); + }); + } } diff --git a/src/OpenApi/test/SharedTypes.cs b/src/OpenApi/test/SharedTypes.cs index c691f54acd30..edf8f45c29b5 100644 --- a/src/OpenApi/test/SharedTypes.cs +++ b/src/OpenApi/test/SharedTypes.cs @@ -63,3 +63,12 @@ internal class Proposal public required Proposal ProposalElement { get; set; } public required Stream Stream { get; set; } } + +internal class PaginatedItems(int pageIndex, int pageSize, long totalItems, int totalPages, IEnumerable items) where T : class +{ + public int PageIndex { get; set; } = pageIndex; + public int PageSize { get; set; } = pageSize; + public long TotalItems { get; set; } = totalItems; + public int TotalPages { get; set; } = totalPages; + public IEnumerable Items { get; set; } = items; +}