From 2554006b23de2347dd77c2c150815deafa1ee279 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Tue, 29 Jun 2021 14:49:38 +0100 Subject: [PATCH] Add ToCloudEventBatch to HttpListenerExtensions Fixes #166 Signed-off-by: Jon Skeet --- .../Http/HttpListenerExtensions.cs | 71 +++++++++++++++++++ .../Http/HttpClientExtensionsTest.cs | 4 +- .../Http/HttpListenerExtensionsTest.cs | 42 +++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs index c391d38..3b6991a 100644 --- a/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs +++ b/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs @@ -212,6 +212,77 @@ namespace CloudNative.CloudEvents.Http } } + /// + /// Converts this HTTP request message into a CloudEvent batch. + /// + /// The HTTP request to convert. Must not be null. + /// The event formatter to use to parse the CloudEvents. Must not be null. + /// The extension attributes to use when parsing the CloudEvents. May be null. + /// The decoded batch of CloudEvents. + public static Task> ToCloudEventBatchAsync( + this HttpListenerRequest httpListenerRequest, + CloudEventFormatter formatter, + params CloudEventAttribute[] extensionAttributes) => + ToCloudEventBatchAsync(httpListenerRequest, formatter, (IEnumerable)extensionAttributes); + + /// + /// Converts this HTTP request message into a CloudEvent batch. + /// + /// The HTTP request to convert. Must not be null. + /// The event formatter to use to parse the CloudEvent. Must not be null. + /// The extension attributes to use when parsing the CloudEvent. May be null. + /// The decoded batch of CloudEvents. + public static async Task> ToCloudEventBatchAsync( + this HttpListenerRequest httpListenerRequest, + CloudEventFormatter formatter, + IEnumerable extensionAttributes) => + await ToCloudEventBatchInternalAsync(httpListenerRequest, formatter, extensionAttributes, async: true).ConfigureAwait(false); + + /// + /// Converts this HTTP request message into a CloudEvent batch. + /// + /// The HTTP request to convert. Must not be null. + /// The event formatter to use to parse the CloudEvents. Must not be null. + /// The extension attributes to use when parsing the CloudEvents. May be null. + /// The decoded batch of CloudEvents. + public static IReadOnlyList ToCloudEventBatch( + this HttpListenerRequest httpListenerRequest, + CloudEventFormatter formatter, + params CloudEventAttribute[] extensionAttributes) => + ToCloudEventBatch(httpListenerRequest, formatter, (IEnumerable) extensionAttributes); + + /// + /// Converts this HTTP request message into a CloudEvent batch. + /// + /// The HTTP request to convert. Must not be null. + /// The event formatter to use to parse the CloudEvents. Must not be null. + /// The extension attributes to use when parsing the CloudEvents. May be null. + /// The decoded batch of CloudEvents. + public static IReadOnlyList ToCloudEventBatch( + this HttpListenerRequest httpListenerRequest, + CloudEventFormatter formatter, + IEnumerable extensionAttributes) => + ToCloudEventBatchInternalAsync(httpListenerRequest, formatter, extensionAttributes, async: false).GetAwaiter().GetResult(); + + private async static Task> ToCloudEventBatchInternalAsync(HttpListenerRequest httpListenerRequest, + CloudEventFormatter formatter, IEnumerable extensionAttributes, bool async) + { + Validation.CheckNotNull(httpListenerRequest, nameof(httpListenerRequest)); + Validation.CheckNotNull(formatter, nameof(formatter)); + + if (HasCloudEventsBatchContentType(httpListenerRequest)) + { + var contentType = MimeUtilities.CreateContentTypeOrNull(httpListenerRequest.ContentType); + return async + ? await formatter.DecodeBatchModeMessageAsync(httpListenerRequest.InputStream, contentType, extensionAttributes).ConfigureAwait(false) + : formatter.DecodeBatchModeMessage(httpListenerRequest.InputStream, contentType, extensionAttributes); + } + else + { + throw new ArgumentException("HTTP message does not represent a CloudEvents batch.", nameof(httpListenerRequest)); + } + } + private static bool HasCloudEventsContentType(HttpListenerRequest request) => MimeUtilities.IsCloudEventsContentType(request.ContentType); diff --git a/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs b/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs index a6a327e..026d2a5 100644 --- a/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs @@ -438,7 +438,7 @@ namespace CloudNative.CloudEvents.Http.UnitTests } } - private static HttpRequestMessage CreateRequestMessage(ReadOnlyMemory content, ContentType contentType) => + internal static HttpRequestMessage CreateRequestMessage(ReadOnlyMemory content, ContentType contentType) => new HttpRequestMessage { Content = new ByteArrayContent(content.ToArray()) @@ -447,7 +447,7 @@ namespace CloudNative.CloudEvents.Http.UnitTests } }; - private static HttpResponseMessage CreateResponseMessage(ReadOnlyMemory content, ContentType contentType) => + internal static HttpResponseMessage CreateResponseMessage(ReadOnlyMemory content, ContentType contentType) => new HttpResponseMessage { Content = new ByteArrayContent(content.ToArray()) diff --git a/test/CloudNative.CloudEvents.UnitTests/Http/HttpListenerExtensionsTest.cs b/test/CloudNative.CloudEvents.UnitTests/Http/HttpListenerExtensionsTest.cs index 856a14b..8c9d29b 100644 --- a/test/CloudNative.CloudEvents.UnitTests/Http/HttpListenerExtensionsTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/Http/HttpListenerExtensionsTest.cs @@ -141,6 +141,48 @@ namespace CloudNative.CloudEvents.Http.UnitTests Assert.Equal(originalCloudEvent.Data, parsedCloudEvent.Data); } + [Fact] + public async Task ToCloudEventBatch_Valid() + { + var batch = CreateSampleBatch(); + + var formatter = new JsonEventFormatter(); + var contentBytes = formatter.EncodeBatchModeMessage(batch, out var contentType); + + AssertBatchesEqual(batch, await GetBatchAsync(context => context.Request.ToCloudEventBatchAsync(formatter, EmptyExtensionArray))); + AssertBatchesEqual(batch, await GetBatchAsync(context => context.Request.ToCloudEventBatchAsync(formatter, EmptyExtensionSequence))); + AssertBatchesEqual(batch, await GetBatchAsync(context => Task.FromResult(context.Request.ToCloudEventBatch(formatter, EmptyExtensionArray)))); + AssertBatchesEqual(batch, await GetBatchAsync(context => Task.FromResult(context.Request.ToCloudEventBatch(formatter, EmptyExtensionSequence)))); + + Task> GetBatchAsync(Func>> handler) + { + var request = HttpClientExtensionsTest.CreateRequestMessage(contentBytes, contentType); + request.RequestUri = new Uri(ListenerAddress); + return SendRequestAsync(request, handler); + } + } + + [Fact] + public async Task ToCloudEventBatchAsync_Invalid() + { + // Most likely accident: calling ToCloudEventBatchAsync with a single event in structured mode. + var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); + var formatter = new JsonEventFormatter(); + var contentBytes = formatter.EncodeStructuredModeMessage(cloudEvent, out var contentType); + + await ExpectFailure(context => context.Request.ToCloudEventBatchAsync(formatter, EmptyExtensionArray)); + await ExpectFailure(context => context.Request.ToCloudEventBatchAsync(formatter, EmptyExtensionSequence)); + await ExpectFailure(context => Task.FromResult(context.Request.ToCloudEventBatch(formatter, EmptyExtensionArray))); + await ExpectFailure(context => Task.FromResult(context.Request.ToCloudEventBatch(formatter, EmptyExtensionSequence))); + + async Task ExpectFailure(Func>> handler) + { + var request = HttpClientExtensionsTest.CreateRequestMessage(contentBytes, contentType); + request.RequestUri = new Uri(ListenerAddress); + await SendRequestAsync(request, context => Assert.ThrowsAsync(() => handler(context))); + } + } + [Fact] public async Task CopyToHttpListenerResponseAsync_BinaryMode() {