Skip to content

Commit

Permalink
Add test coverage for generic types (#55940)
Browse files Browse the repository at this point in the history
  • Loading branch information
captainsafia authored Jun 3, 2024
1 parent b8566e1 commit afa0b36
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
var props = ReadDictionary<OpenApiJsonSchema>(ref reader);
schema.Properties = props?.ToDictionary(p => p.Key, p => p.Value.Schema);
break;
case OpenApiSchemaKeywords.AdditionalPropertiesKeyword:
reader.Read();
var additionalPropsConverter = (JsonConverter<OpenApiJsonSchema>)options.GetTypeInfo(typeof(OpenApiJsonSchema)).Converter;
schema.AdditionalProperties = additionalPropsConverter.Read(ref reader, typeof(OpenApiJsonSchema), options)?.Schema;
break;
case OpenApiSchemaKeywords.AnyOfKeyword:
reader.Read();
schema.Type = "object";
Expand All @@ -284,6 +289,9 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
var mappings = ReadDictionary<string>(ref reader);
schema.Discriminator.Mapping = mappings;
break;
default:
reader.Skip();
break;
}
}
}
1 change: 1 addition & 0 deletions src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,4 +393,140 @@ await VerifyOpenApiDocument(builder, document =>
});
});
}

[Fact]
public async Task GetOpenApiResponse_HandlesGenericType()
{
// Arrange
var builder = CreateBuilder();

// Act
builder.MapGet("/", () => TypedResults.Ok<PaginatedItems<Todo>>(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<string, string[]>
{
["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);
});
});
}
}
9 changes: 9 additions & 0 deletions src/OpenApi/test/SharedTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,12 @@ internal class Proposal
public required Proposal ProposalElement { get; set; }
public required Stream Stream { get; set; }
}

internal class PaginatedItems<T>(int pageIndex, int pageSize, long totalItems, int totalPages, IEnumerable<T> 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<T> Items { get; set; } = items;
}

0 comments on commit afa0b36

Please sign in to comment.