Skip to content

Commit 32f8882

Browse files
committed
Add cancellation and async tests to transformers
1 parent becb36a commit 32f8882

File tree

4 files changed

+266
-6
lines changed

4 files changed

+266
-6
lines changed

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33

44
using System.Reflection;
55
using System.Text;
6-
using System.Text.Json;
76
using Microsoft.AspNetCore.Builder;
87
using Microsoft.AspNetCore.Hosting.Server;
98
using Microsoft.AspNetCore.Hosting.Server.Features;
109
using Microsoft.AspNetCore.Http.Features;
11-
using Microsoft.AspNetCore.Http.Json;
1210
using Microsoft.AspNetCore.Mvc;
1311
using Microsoft.AspNetCore.Mvc.Abstractions;
1412
using Microsoft.AspNetCore.Mvc.ActionConstraints;
@@ -29,8 +27,8 @@
2927

3028
public abstract class OpenApiDocumentServiceTestBase
3129
{
32-
public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Action<OpenApiDocument> verifyOpenApiDocument, CancellationToken cancellationToken = default)
33-
=> await VerifyOpenApiDocument(builder, new OpenApiOptions(), verifyOpenApiDocument, cancellationToken);
30+
public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Action<OpenApiDocument> verifyOpenApiDocument)
31+
=> await VerifyOpenApiDocument(builder, new OpenApiOptions(), verifyOpenApiDocument);
3432

3533
public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, OpenApiOptions openApiOptions, Action<OpenApiDocument> verifyOpenApiDocument, CancellationToken cancellationToken = default)
3634
{
@@ -40,12 +38,12 @@ public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Op
4038
verifyOpenApiDocument(document);
4139
}
4240

43-
public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action<OpenApiDocument> verifyOpenApiDocument)
41+
public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action<OpenApiDocument> verifyOpenApiDocument, CancellationToken cancellationToken = default)
4442
{
4543
var builder = CreateBuilder();
4644
var documentService = CreateDocumentService(builder, action);
4745
var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope();
48-
var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider);
46+
var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, cancellationToken);
4947
verifyOpenApiDocument(document);
5048
}
5149

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/DocumentTransformerTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,94 @@ public async Task DocumentTransformer_CanAccessTransientServiceFromContextApplic
240240
Assert.Equal(2, Dependency.InstantiationCount);
241241
}
242242

243+
[Fact]
244+
public async Task DocumentTransformer_RespectsOperationCancellation()
245+
{
246+
var builder = CreateBuilder();
247+
builder.MapGet("/todo", () => { });
248+
249+
var options = new OpenApiOptions();
250+
var transformerCalled = false;
251+
var exceptionThrown = false;
252+
253+
options.AddDocumentTransformer(async (document, context, cancellationToken) =>
254+
{
255+
transformerCalled = true;
256+
try
257+
{
258+
await Task.Delay(5000, cancellationToken);
259+
document.Info.Description = "Should not be set";
260+
}
261+
catch (OperationCanceledException)
262+
{
263+
exceptionThrown = true;
264+
throw;
265+
}
266+
});
267+
268+
using var cts = new CancellationTokenSource();
269+
cts.CancelAfter(100);
270+
271+
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
272+
{
273+
await VerifyOpenApiDocument(builder, options, _ => { }, cts.Token);
274+
});
275+
276+
Assert.True(transformerCalled);
277+
Assert.True(exceptionThrown);
278+
}
279+
280+
[Fact]
281+
public async Task DocumentTransformer_ExecutesAsynchronously()
282+
{
283+
var builder = CreateBuilder();
284+
builder.MapGet("/todo", () => { });
285+
286+
var options = new OpenApiOptions();
287+
var transformerOrder = new List<int>();
288+
var tcs1 = new TaskCompletionSource();
289+
var tcs2 = new TaskCompletionSource();
290+
var tcs2Called = false;
291+
292+
options.AddDocumentTransformer(async (document, context, cancellationToken) =>
293+
{
294+
await tcs1.Task;
295+
transformerOrder.Add(1);
296+
document.Info.Title = "First";
297+
});
298+
299+
options.AddDocumentTransformer((document, context, cancellationToken) =>
300+
{
301+
transformerOrder.Add(2);
302+
document.Info.Title += " Second";
303+
if (!tcs2Called)
304+
{
305+
tcs2Called = true;
306+
tcs2.TrySetResult();
307+
}
308+
return Task.CompletedTask;
309+
});
310+
311+
options.AddDocumentTransformer(async (document, context, cancellationToken) =>
312+
{
313+
await tcs2.Task;
314+
transformerOrder.Add(3);
315+
document.Info.Title += " Third";
316+
});
317+
318+
var documentTask = VerifyOpenApiDocument(builder, options, document =>
319+
{
320+
Assert.Equal("First Second Third", document.Info.Title);
321+
});
322+
323+
await Task.Delay(100);
324+
tcs1.TrySetResult();
325+
326+
await documentTask;
327+
328+
Assert.Equal([1, 2, 3], transformerOrder);
329+
}
330+
243331
private class ActivatedTransformer : IOpenApiDocumentTransformer
244332
{
245333
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OperationTransformerTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,92 @@ await VerifyOpenApiDocument(builder, document =>
571571
});
572572
}
573573

574+
[Fact]
575+
public async Task OperationTransformer_RespectsOperationCancellation()
576+
{
577+
var builder = CreateBuilder();
578+
builder.MapGet("/todo", () => { });
579+
580+
var options = new OpenApiOptions();
581+
var transformerCalled = false;
582+
var exceptionThrown = false;
583+
584+
options.AddOperationTransformer(async (operation, context, cancellationToken) =>
585+
{
586+
transformerCalled = true;
587+
try
588+
{
589+
await Task.Delay(5000, cancellationToken);
590+
operation.Description = "Should not be set";
591+
}
592+
catch (OperationCanceledException)
593+
{
594+
exceptionThrown = true;
595+
throw;
596+
}
597+
});
598+
599+
using var cts = new CancellationTokenSource();
600+
cts.CancelAfter(100);
601+
602+
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
603+
{
604+
await VerifyOpenApiDocument(builder, options, _ => { }, cts.Token);
605+
});
606+
607+
Assert.True(transformerCalled);
608+
Assert.True(exceptionThrown);
609+
}
610+
611+
[Fact]
612+
public async Task OperationTransformer_ExecutesAsynchronously()
613+
{
614+
var builder = CreateBuilder();
615+
builder.MapGet("/todo", () => { });
616+
617+
var options = new OpenApiOptions();
618+
var transformerOrder = new List<int>();
619+
var tcs1 = new TaskCompletionSource();
620+
var tcs2 = new TaskCompletionSource();
621+
622+
options.AddOperationTransformer(async (operation, context, cancellationToken) =>
623+
{
624+
await tcs1.Task;
625+
transformerOrder.Add(1);
626+
operation.Description = "First";
627+
});
628+
629+
options.AddOperationTransformer((operation, context, cancellationToken) =>
630+
{
631+
transformerOrder.Add(2);
632+
operation.Description += " Second";
633+
tcs2.TrySetResult();
634+
return Task.CompletedTask;
635+
});
636+
637+
options.AddOperationTransformer(async (operation, context, cancellationToken) =>
638+
{
639+
await tcs2.Task;
640+
transformerOrder.Add(3);
641+
operation.Description += " Third";
642+
});
643+
644+
var documentTask = VerifyOpenApiDocument(builder, options, document =>
645+
{
646+
var operation = Assert.Single(document.Paths["/todo"].Operations.Values);
647+
Assert.Equal("First Second Third", operation.Description);
648+
});
649+
650+
await Task.Delay(100);
651+
tcs1.TrySetResult();
652+
653+
await documentTask;
654+
655+
// Verify transformers executed in the correct order, once for each transformer
656+
// since there is a single operation in the document.
657+
Assert.Equal([1, 2, 3], transformerOrder);
658+
}
659+
574660
private class ActivatedTransformer : IOpenApiOperationTransformer
575661
{
576662
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,94 @@ public async Task SchemaTransformer_CanAccessTransientServiceFromContextApplicat
848848
Assert.Equal(10, Dependency.InstantiationCount);
849849
}
850850

851+
[Fact]
852+
public async Task SchemaTransformer_RespectsOperationCancellation()
853+
{
854+
var builder = CreateBuilder();
855+
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
856+
857+
var options = new OpenApiOptions();
858+
var transformerCalled = false;
859+
var exceptionThrown = false;
860+
861+
options.AddSchemaTransformer(async (schema, context, cancellationToken) =>
862+
{
863+
transformerCalled = true;
864+
try
865+
{
866+
await Task.Delay(5000, cancellationToken);
867+
schema.Description = "Should not be set";
868+
}
869+
catch (OperationCanceledException)
870+
{
871+
exceptionThrown = true;
872+
throw;
873+
}
874+
});
875+
876+
using var cts = new CancellationTokenSource();
877+
cts.CancelAfter(100);
878+
879+
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
880+
{
881+
await VerifyOpenApiDocument(builder, options, _ => { }, cts.Token);
882+
});
883+
884+
Assert.True(transformerCalled);
885+
Assert.True(exceptionThrown);
886+
}
887+
888+
[Fact]
889+
public async Task SchemaTransformer_ExecutesAsynchronously()
890+
{
891+
var builder = CreateBuilder();
892+
builder.MapGet("/todo", () => new Todo(1, "Item1", false, DateTime.Now));
893+
894+
var options = new OpenApiOptions();
895+
var transformerOrder = new List<int>();
896+
var tcs1 = new TaskCompletionSource();
897+
var tcs2 = new TaskCompletionSource();
898+
899+
// Assert that transformers wait for completion signal from sibling tasks before running
900+
options.AddSchemaTransformer(async (schema, context, cancellationToken) =>
901+
{
902+
await tcs1.Task;
903+
transformerOrder.Add(1);
904+
schema.Description = "First";
905+
});
906+
907+
options.AddSchemaTransformer((schema, context, cancellationToken) =>
908+
{
909+
transformerOrder.Add(2);
910+
schema.Description += " Second";
911+
tcs2.TrySetResult();
912+
return Task.CompletedTask;
913+
});
914+
915+
options.AddSchemaTransformer(async (schema, context, cancellationToken) =>
916+
{
917+
await tcs2.Task;
918+
transformerOrder.Add(3);
919+
schema.Description += " Third";
920+
});
921+
922+
var documentTask = VerifyOpenApiDocument(builder, options, document =>
923+
{
924+
var operation = Assert.Single(document.Paths["/todo"].Operations.Values);
925+
var schema = operation.Responses["200"].Content["application/json"].Schema;
926+
Assert.Equal("First Second Third", schema.Description);
927+
});
928+
929+
await Task.Delay(100);
930+
tcs1.TrySetResult();
931+
932+
await documentTask;
933+
934+
// Each transformer is called a total of 5 times, once for the top-level schema
935+
// and one for each of the four properties within the Todo type.
936+
Assert.Equal([1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3], transformerOrder);
937+
}
938+
851939
private class PolymorphicContainer
852940
{
853941
public string Name { get; }

0 commit comments

Comments
 (0)