/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Http/Http.Results/test/ServerSentEventsResultTests.cs
362 строки
13 KB
Safia Abdalla
Add support for ServerSentEventsResult and extension methods (#60616)
03 мар 2025, 20:20
Не верифицирован
03 мар 2025, 20:20
bd1ba76
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO.Pipelines; using System.Net.ServerSentEvents; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Http.HttpResults; public class ServerSentEventsResultTests { [Fact] public async Task ExecuteAsync_SetsContentTypeAndHeaders() { // Arrange var httpContext = GetHttpContext(); var events = AsyncEnumerable.Empty<string>(); var result = TypedResults.ServerSentEvents(events); // Act await result.ExecuteAsync(httpContext); // Assert Assert.Equal("text/event-stream", httpContext.Response.ContentType); Assert.Equal("no-cache,no-store", httpContext.Response.Headers.CacheControl); Assert.Equal("no-cache", httpContext.Response.Headers.Pragma); Assert.Equal("identity", httpContext.Response.Headers.ContentEncoding); } [Fact] public async Task ExecuteAsync_WritesStringEventsToResponse() { // Arrange var httpContext = GetHttpContext(); var events = new[] { "event1\"with\"quotes", "event2" }.ToAsyncEnumerable(); var result = TypedResults.ServerSentEvents(events); // Act await result.ExecuteAsync(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains("data: event1\"with\"quotes\n\n", responseBody); Assert.Contains("data: event2\n\n", responseBody); // Verify strings are not JSON serialized Assert.DoesNotContain("data: \"event1", responseBody); Assert.DoesNotContain("data: \"event2", responseBody); } [Fact] public async Task ExecuteAsync_WritesStringsEventsWithEventType() { // Arrange var httpContext = GetHttpContext(); var events = new[] { "event1", "event2" }.ToAsyncEnumerable(); var result = TypedResults.ServerSentEvents(events, "test-event"); // Act await result.ExecuteAsync(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains("event: test-event\ndata: event1\n\n", responseBody); Assert.Contains("event: test-event\ndata: event2\n\n", responseBody); } [Fact] public async Task ExecuteAsync_WithSseItems_WritesStringEventsDirectly() { // Arrange var httpContext = GetHttpContext(); var events = new[] { new SseItem<string>("event1", "custom-event") }.ToAsyncEnumerable(); var result = TypedResults.ServerSentEvents(events); // Act await result.ExecuteAsync(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains("event: custom-event\n", responseBody); Assert.Contains("data: event1\n\n", responseBody); } [Fact] public async Task ExecuteAsync_ThrowsArgumentNullException_WhenHttpContextIsNull() { // Arrange var events = AsyncEnumerable.Empty<string>(); var result = TypedResults.ServerSentEvents(events); HttpContext httpContext = null; // Act & Assert await Assert.ThrowsAsync<ArgumentNullException>("httpContext", () => result.ExecuteAsync(httpContext)); } [Fact] public async Task ExecuteAsync_HandlesNullData() { // Arrange var httpContext = GetHttpContext(); var events = new string[] { null }.ToAsyncEnumerable(); var result = TypedResults.ServerSentEvents(events); // Act await result.ExecuteAsync(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains("data: \n\n", responseBody); } [Fact] public void PopulateMetadata_AddsResponseTypeMetadata() { // Arrange ServerSentEventsResult<string> MyApi() { throw new NotImplementedException(); } var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0); // Act PopulateMetadata<ServerSentEventsResult<string>>(((Delegate)MyApi).GetMethodInfo(), builder); // Assert var producesResponseTypeMetadata = builder.Metadata.OfType<ProducesResponseTypeMetadata>().Last(); Assert.Equal(StatusCodes.Status200OK, producesResponseTypeMetadata.StatusCode); Assert.Equal(typeof(SseItem<string>), producesResponseTypeMetadata.Type); Assert.Collection(producesResponseTypeMetadata.ContentTypes, contentType => Assert.Equal("text/event-stream", contentType)); } [Fact] public async Task ExecuteAsync_WithObjectData_SerializesAsJson() { // Arrange var httpContext = GetHttpContext(); var testObject = new TestObject { Name = "Test", Value = 42 }; var events = new[] { testObject }.ToAsyncEnumerable(); var result = TypedResults.ServerSentEvents(events); // Act await result.ExecuteAsync(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains(@"data: {""name"":""Test"",""value"":42}", responseBody); } [Fact] public async Task ExecuteAsync_WithSsItems_SerializesDataAsJson() { // Arrange var httpContext = GetHttpContext(); var testObject = new TestObject { Name = "Test", Value = 42 }; var events = new[] { new SseItem<TestObject>(testObject) }.ToAsyncEnumerable(); var result = TypedResults.ServerSentEvents(events); // Act await result.ExecuteAsync(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains(@"data: {""name"":""Test"",""value"":42}", responseBody); } [Fact] public async Task ExecuteAsync_WithCustomJsonOptions_UsesConfiguredOptions() { // Arrange var services = new ServiceCollection(); services.AddLogging(); services.Configure<JsonOptions>(options => { options.SerializerOptions.PropertyNamingPolicy = null; // Use PascalCase }); var httpContext = new DefaultHttpContext { Response = { Body = new MemoryStream() }, RequestServices = services.BuildServiceProvider() }; var testObject = new TestObject { Name = "Test", Value = 42 }; var events = new[] { testObject }.ToAsyncEnumerable(); var result = TypedResults.ServerSentEvents(events); // Act await result.ExecuteAsync(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains(@"data: {""Name"":""Test"",""Value"":42}", responseBody); } [Fact] public async Task ExecuteAsync_WithPolymorphicType_SerializesCorrectly() { // Arrange var httpContext = GetHttpContext(); var baseClass = new DerivedTestObject { Name = "Test", Value = 42, Extra = "Additional" }; var events = new TestObject[] { baseClass }.ToAsyncEnumerable(); var result = TypedResults.ServerSentEvents(events); // Act await result.ExecuteAsync(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains(@"data: {""extra"":""Additional"",""name"":""Test"",""value"":42}", responseBody); } [Fact] public async Task ExecuteAsync_ObservesCancellationViaRequestAborted() { // Arrange var cts = new CancellationTokenSource(); var httpContext = GetHttpContext(); httpContext.RequestAborted = cts.Token; var firstEventReceived = new TaskCompletionSource(); var secondEventAttempted = new TaskCompletionSource(); var cancellationObserved = new TaskCompletionSource(); var events = GetEvents(cts.Token); var result = TypedResults.ServerSentEvents(events); // Act & Assert var executeTask = result.ExecuteAsync(httpContext); // Wait for first event to be processed then cancel the request and wait // to observe the cancellation await firstEventReceived.Task; cts.Cancel(); await secondEventAttempted.Task; // Verify the execution was cancelled and only the first event was written await Assert.ThrowsAsync<TaskCanceledException>(() => executeTask); var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains("data: event1\n\n", responseBody); Assert.DoesNotContain("data: event2\n\n", responseBody); async IAsyncEnumerable<string> GetEvents([EnumeratorCancellation] CancellationToken cancellationToken = default) { try { yield return "event1"; firstEventReceived.SetResult(); cancellationToken.Register(cancellationObserved.SetResult); await cancellationObserved.Task; yield return "event2"; } finally { secondEventAttempted.SetResult(); } } } [Fact] public async Task ExecuteAsync_DisablesBuffering() { // Arrange var httpContext = GetHttpContext(); var events = AsyncEnumerable.Empty<string>(); var result = TypedResults.ServerSentEvents(events); var bufferingDisabled = false; var mockBufferingFeature = new MockHttpResponseBodyFeature( onDisableBuffering: () => bufferingDisabled = true); httpContext.Features.Set<IHttpResponseBodyFeature>(mockBufferingFeature); // Act await result.ExecuteAsync(httpContext); // Assert Assert.True(bufferingDisabled); } [Fact] public async Task ExecuteAsync_WithByteArrayData_WritesDataDirectly() { // Arrange var httpContext = GetHttpContext(); var bytes = "event1"u8.ToArray(); var events = new[] { new SseItem<byte[]>(bytes) }.ToAsyncEnumerable(); var result = TypedResults.ServerSentEvents(events); // Act await result.ExecuteAsync(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains("data: event1\n\n", responseBody); // Assert that string is not JSON serialized Assert.DoesNotContain("data: \"event1", responseBody); } [Fact] public async Task ExecuteAsync_WithByteArrayData_HandlesNullData() { // Arrange var httpContext = GetHttpContext(); var events = new[] { new SseItem<byte[]>(null) }.ToAsyncEnumerable(); var result = TypedResults.ServerSentEvents(events); // Act await result.ExecuteAsync(httpContext); // Assert var responseBody = Encoding.UTF8.GetString(((MemoryStream)httpContext.Response.Body).ToArray()); Assert.Contains("data: \n\n", responseBody); } private class MockHttpResponseBodyFeature(Action onDisableBuffering) : IHttpResponseBodyFeature { public Stream Stream => new MemoryStream(); public PipeWriter Writer => throw new NotImplementedException(); public Task CompleteAsync() => throw new NotImplementedException(); public void DisableBuffering() => onDisableBuffering(); public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task StartAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } private static void PopulateMetadata<TResult>(MethodInfo method, EndpointBuilder builder) where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder); private static DefaultHttpContext GetHttpContext() { var httpContext = new DefaultHttpContext(); httpContext.Response.Body = new MemoryStream(); httpContext.RequestServices = CreateServices(); return httpContext; } private static ServiceProvider CreateServices() { var services = new ServiceCollection(); services.AddLogging(); return services.BuildServiceProvider(); } private class TestObject { public string Name { get; set; } public int Value { get; set; } } private class DerivedTestObject : TestObject { public string Extra { get; set; } } }