Skip to content

Commit afa0b36

Browse files
authored
Add test coverage for generic types (#55940)
1 parent b8566e1 commit afa0b36

File tree

4 files changed

+154
-0
lines changed

4 files changed

+154
-0
lines changed

src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,11 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
265265
var props = ReadDictionary<OpenApiJsonSchema>(ref reader);
266266
schema.Properties = props?.ToDictionary(p => p.Key, p => p.Value.Schema);
267267
break;
268+
case OpenApiSchemaKeywords.AdditionalPropertiesKeyword:
269+
reader.Read();
270+
var additionalPropsConverter = (JsonConverter<OpenApiJsonSchema>)options.GetTypeInfo(typeof(OpenApiJsonSchema)).Converter;
271+
schema.AdditionalProperties = additionalPropsConverter.Read(ref reader, typeof(OpenApiJsonSchema), options)?.Schema;
272+
break;
268273
case OpenApiSchemaKeywords.AnyOfKeyword:
269274
reader.Read();
270275
schema.Type = "object";
@@ -284,6 +289,9 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
284289
var mappings = ReadDictionary<string>(ref reader);
285290
schema.Discriminator.Mapping = mappings;
286291
break;
292+
default:
293+
reader.Skip();
294+
break;
287295
}
288296
}
289297
}

src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ internal class OpenApiSchemaKeywords
77
public const string FormatKeyword = "format";
88
public const string ItemsKeyword = "items";
99
public const string PropertiesKeyword = "properties";
10+
public const string AdditionalPropertiesKeyword = "additionalProperties";
1011
public const string RequiredKeyword = "required";
1112
public const string AnyOfKeyword = "anyOf";
1213
public const string EnumKeyword = "enum";

src/OpenApi/test/Services/OpenApiComponentService/OpenApiComponentService.ResponseSchemas.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,4 +393,140 @@ await VerifyOpenApiDocument(builder, document =>
393393
});
394394
});
395395
}
396+
397+
[Fact]
398+
public async Task GetOpenApiResponse_HandlesGenericType()
399+
{
400+
// Arrange
401+
var builder = CreateBuilder();
402+
403+
// Act
404+
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)])));
405+
406+
// Assert that the response schema is correctly generated. For now, generics are inlined
407+
// in the generated OpenAPI schema since OpenAPI supports generics via dynamic references as of
408+
// OpenAPI 3.1.0.
409+
await VerifyOpenApiDocument(builder, document =>
410+
{
411+
var operation = document.Paths["/"].Operations[OperationType.Get];
412+
var responses = Assert.Single(operation.Responses);
413+
var response = responses.Value;
414+
Assert.True(response.Content.TryGetValue("application/json", out var mediaType));
415+
Assert.Equal("object", mediaType.Schema.Type);
416+
Assert.Collection(mediaType.Schema.Properties,
417+
property =>
418+
{
419+
Assert.Equal("pageIndex", property.Key);
420+
Assert.Equal("integer", property.Value.Type);
421+
Assert.Equal("int32", property.Value.Format);
422+
},
423+
property =>
424+
{
425+
Assert.Equal("pageSize", property.Key);
426+
Assert.Equal("integer", property.Value.Type);
427+
Assert.Equal("int32", property.Value.Format);
428+
},
429+
property =>
430+
{
431+
Assert.Equal("totalItems", property.Key);
432+
Assert.Equal("integer", property.Value.Type);
433+
Assert.Equal("int64", property.Value.Format);
434+
},
435+
property =>
436+
{
437+
Assert.Equal("totalPages", property.Key);
438+
Assert.Equal("integer", property.Value.Type);
439+
Assert.Equal("int32", property.Value.Format);
440+
},
441+
property =>
442+
{
443+
Assert.Equal("items", property.Key);
444+
Assert.Equal("array", property.Value.Type);
445+
Assert.NotNull(property.Value.Items);
446+
Assert.Equal("object", property.Value.Items.Type);
447+
Assert.Collection(property.Value.Items.Properties,
448+
property =>
449+
{
450+
Assert.Equal("id", property.Key);
451+
Assert.Equal("integer", property.Value.Type);
452+
Assert.Equal("int32", property.Value.Format);
453+
},
454+
property =>
455+
{
456+
Assert.Equal("title", property.Key);
457+
Assert.Equal("string", property.Value.Type);
458+
},
459+
property =>
460+
{
461+
Assert.Equal("completed", property.Key);
462+
Assert.Equal("boolean", property.Value.Type);
463+
},
464+
property =>
465+
{
466+
Assert.Equal("createdAt", property.Key);
467+
Assert.Equal("string", property.Value.Type);
468+
Assert.Equal("date-time", property.Value.Format);
469+
});
470+
});
471+
});
472+
}
473+
474+
[Fact]
475+
public async Task GetOpenApiResponse_HandlesValidationProblem()
476+
{
477+
// Arrange
478+
var builder = CreateBuilder();
479+
480+
// Act
481+
builder.MapGet("/", () => TypedResults.ValidationProblem(new Dictionary<string, string[]>
482+
{
483+
["Name"] = ["Name is required"]
484+
}));
485+
486+
// Assert
487+
await VerifyOpenApiDocument(builder, document =>
488+
{
489+
var operation = document.Paths["/"].Operations[OperationType.Get];
490+
var responses = Assert.Single(operation.Responses);
491+
var response = responses.Value;
492+
Assert.True(response.Content.TryGetValue("application/problem+json", out var mediaType));
493+
Assert.Equal("object", mediaType.Schema.Type);
494+
Assert.Collection(mediaType.Schema.Properties,
495+
property =>
496+
{
497+
Assert.Equal("type", property.Key);
498+
Assert.Equal("string", property.Value.Type);
499+
},
500+
property =>
501+
{
502+
Assert.Equal("title", property.Key);
503+
Assert.Equal("string", property.Value.Type);
504+
},
505+
property =>
506+
{
507+
Assert.Equal("status", property.Key);
508+
Assert.Equal("integer", property.Value.Type);
509+
Assert.Equal("int32", property.Value.Format);
510+
},
511+
property =>
512+
{
513+
Assert.Equal("detail", property.Key);
514+
Assert.Equal("string", property.Value.Type);
515+
},
516+
property =>
517+
{
518+
Assert.Equal("instance", property.Key);
519+
Assert.Equal("string", property.Value.Type);
520+
},
521+
property =>
522+
{
523+
Assert.Equal("errors", property.Key);
524+
Assert.Equal("object", property.Value.Type);
525+
// The errors object is a dictionary of string[]. Use `additionalProperties`
526+
// to indicate that the payload can be arbitrary keys with string[] values.
527+
Assert.Equal("array", property.Value.AdditionalProperties.Type);
528+
Assert.Equal("string", property.Value.AdditionalProperties.Items.Type);
529+
});
530+
});
531+
}
396532
}

src/OpenApi/test/SharedTypes.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,12 @@ internal class Proposal
6363
public required Proposal ProposalElement { get; set; }
6464
public required Stream Stream { get; set; }
6565
}
66+
67+
internal class PaginatedItems<T>(int pageIndex, int pageSize, long totalItems, int totalPages, IEnumerable<T> items) where T : class
68+
{
69+
public int PageIndex { get; set; } = pageIndex;
70+
public int PageSize { get; set; } = pageSize;
71+
public long TotalItems { get; set; } = totalItems;
72+
public int TotalPages { get; set; } = totalPages;
73+
public IEnumerable<T> Items { get; set; } = items;
74+
}

0 commit comments

Comments
 (0)