More test/doc coverage, send sample

Signed-off-by: clemensv <clemensv@microsoft.com>
This commit is contained in:
clemensv 2018-11-26 12:47:56 +01:00
parent 84d9f29b29
commit a650ad5323
8 changed files with 509 additions and 223 deletions

View File

@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpSend", "samples\HttpSend\HttpSend.csproj", "{F1B9B769-DB6B-481F-905C-24FE3B12E00E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -48,6 +50,18 @@ Global
{95215090-BDE3-4628-9261-64B91FBE4665}.Release|x64.Build.0 = Release|Any CPU
{95215090-BDE3-4628-9261-64B91FBE4665}.Release|x86.ActiveCfg = Release|Any CPU
{95215090-BDE3-4628-9261-64B91FBE4665}.Release|x86.Build.0 = Release|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Debug|x64.ActiveCfg = Debug|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Debug|x64.Build.0 = Debug|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Debug|x86.ActiveCfg = Debug|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Debug|x86.Build.0 = Debug|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Release|Any CPU.Build.0 = Release|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Release|x64.ActiveCfg = Release|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Release|x64.Build.0 = Release|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Release|x86.ActiveCfg = Release|Any CPU
{F1B9B769-DB6B-481F-905C-24FE3B12E00E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

134
README.md
View File

@ -43,7 +43,13 @@ If a CloudEvents-prefixed transport header, like an HTTP header, is `string` typ
## Extensions
CloudEvent extensions are reflected by implementations of the `ICloudEventExtension` interface.
CloudEvent extensions are represented by implementations of the `ICloudEventExtension`
interface. The SDK includes strongly typed implementations for all offical CloudEvents
extensions:
* `DistributedTracingExtension` for [distributed tracing](https://github.com/cloudevents/spec/blob/master/extensions/distributed-tracing.md)
* `SampledRateExtension` for [sampled rate](https://github.com/cloudevents/spec/blob/master/extensions/sampled-rate.md)
* `SequenceExtension` for [sequence](https://github.com/cloudevents/spec/blob/master/extensions/sequence.md)
Extension classes provides type-safe access to the extension attributes, and implement the
required validations as well as type mappings. An extension object is always created as an
@ -73,4 +79,130 @@ The extension can later be accessed via the `Extension<T>()` method:
var s = cloudEvent.Extension<DistributedTracingExtension>().TraceParent
```
All APIs where a `CloudEvent` is constructed from an incoming event (or request or
response), allow for extension instances to be added to the respective methods, and
the extensions are invoked in the mapping process, for instance to extract information
from headers that deviate from the CloudEvents default mapping.
For instance, the server-side mapping for `HttpRequestMessage` allows adding
extensions like this:
``` C#
public async Task<HttpResponseMessage> Run( HttpRequestMessage req, ILogger log)
{
var cloudEvent = await req.ToCloudEventAsync(new DistributedTracingExtension());
}
```
## Transport Bindings
This SDK helps with mapping CloudEvents from and to messages or transport frames of
popular .NET clients, but without getting in the way of your application's choices of
whether you want to send an event via HTTP PUT or POST or how you want to handle
settlement of transfers in AMQP or MQTT. The transport binding classes and extensions
therefore don't wrap the send and receive operations; you still use the native
API of the respective library.
### HTTP - System.Net.Http.HttpClient
The .NET [`HttpClient`](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) uses
the [`HttpContent`](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent)
abstraction to wrap payloads for sending requests that carry entity bodies.
This SDK provides a [`CloudEventContent`] class derived from `HttpContent` that can be
created from a `CloudEvent` instance, the desired `ContentMode` and an event formatter.
``` C#
var cloudEvent = new CloudEvent("com.example.myevent", new Uri("urn:example-com:mysource"))
{
ContentType = new ContentType(MediaTypeNames.Application.Json),
Data = JsonConvert.SerializeObject("hey there!")
};
var content = new CloudEventContent( cloudEvent,
ContentMode.Structured,
new JsonEventFormatter());
var httpClient = new HttpClient();
var result = (await httpClient.PostAsync(this.Url, content));
```
For responses, `HttpClient` puts all custom headers onto the `HttpResponseMessage` rather
than on the carried `HttpContent` instance. Therefore, if an event is retrieved with
`HttpClient`, for instance from a queue-like structure, the `CloudEvent` is created from
the response message object rather than the content object using the `ToCloudEvent()`
extension method on `HttpResponseMessage`:
``` C#
var httpClient = new HttpClient();
// delete and receive message from top of the queue
var result = await httpClient.DeleteAsync(new Uri("https://example.com/queue/messages/top"));
if (HttpStatusCode.OK == result.StatusCode) {
var receivedCloudEvent = await result.ToCloudEvent();
}
```
### HTTP - System.Net.HttpWebRequest
If your application uses the `HttpWebRequest` client, you can copy a CloudEvent into
the request structure in structured or binary mode:
``` C#
HttpWebRequest httpWebRequest = WebRequest.CreateHttp("https://example.com/target");
httpWebRequest.Method = "POST";
await httpWebRequest.CopyFromAsync(cloudEvent, ContentMode.Structured, new JsonEventFormatter());
```
Mind that the `Method` property must be set to an HTTP method that allows an entity body
to be sent, otherwise the copy operation will fail.
### HTTP - System.Net.HttpListener (HttpRequestMessage)
On the server-side, you can extract a CloudEvent from the server-side `HttpRequestMessage`
with the `ToCloudEventAsync()` extension. If your code handles `HttpRequestContext`,
you will use the `Request` property:
```C#
var cloudEvent = await context.Request.ToCloudEventAsync();
```
If you use a functions framework that lets you handle `HttpResponseMessage` and return
`HttpResponseMessage`, you will call the extension on the request object directly:
``` C#
public async Task<HttpResponseMessage> Run( HttpRequestMessage req, ILogger log)
{
var cloudEvent = await req.ToCloudEventAsync();
}
```
The extension implementation will read the `ContentType` header of the incoming request and
automatically select the correct built-in event format decoder. Your code can always pass an
overriding format decoder instance as the first argument if needed.
If your HTTP handler needs to return a CloudEvent, you copy the `CloudEvent` into the
response with the `CopyFromAsync()` extension method:
``` C#
var cloudEvent = new CloudEvent("com.example.myevent", new Uri("urn:example-com:mysource"))
{
ContentType = new ContentType(MediaTypeNames.Application.Json),
Data = JsonConvert.SerializeObject("hey there!")
};
await context.Response.CopyFromAsync(cloudEvent,
ContentMode.Structured,
new JsonEventFormatter());
context.Response.StatusCode = (int)HttpStatusCode.OK;
```
### AMQP
TBD
## MQTT
TBD

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.2.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\CloudNative.CloudEvents\CloudNative.CloudEvents.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,47 @@
// Copyright (c) Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
namespace HttpSend
{
using System;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Threading.Tasks;
using CloudNative.CloudEvents;
using McMaster.Extensions.CommandLineUtils;
using Newtonsoft.Json;
class Program
{
[Option(Description = "CloudEvents 'source' (default: urn:example-com:mysource:abc)", LongName = "source",
ShortName = "s")]
string Source { get; } = "urn:example-com:mysource:abc";
[Option(Description = "CloudEvents 'type' (default: com.example.myevent)", LongName = "type", ShortName = "t")]
string Type { get; } = "com.example.myevent";
[Required,Option(Description = "HTTP(S) address to send the event to", LongName = "url", ShortName = "u"),]
Uri Url { get; }
public static int Main(string[] args) => CommandLineApplication.Execute<Program>(args);
async Task OnExecuteAsync()
{
var cloudEvent = new CloudEvent(this.Type, new Uri(this.Source))
{
ContentType = new ContentType(MediaTypeNames.Application.Json),
Data = JsonConvert.SerializeObject("hey there!")
};
var content = new CloudEventContent(cloudEvent, ContentMode.Structured, new JsonEventFormatter());
var httpClient = new HttpClient();
var result = (await httpClient.PostAsync(this.Url, content));
Console.WriteLine(result.StatusCode);
}
}
}

View File

@ -5,29 +5,30 @@
namespace CloudNative.CloudEvents
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
public static class HttpClientExtension
{
const string HttpHeaderPrefix = "ce-";
const string SpecVersionHttpHeader = HttpHeaderPrefix + "specversion";
static JsonEventFormatter jsonFormatter = new JsonEventFormatter();
public static Task CopyFromAsync(this HttpListenerResponse httpListenerResponse, CloudEvent cloudEvent, ContentMode contentMode, ICloudEventFormatter formatter)
public static Task CopyFromAsync(this HttpListenerResponse httpListenerResponse, CloudEvent cloudEvent,
ContentMode contentMode, ICloudEventFormatter formatter)
{
if (contentMode == ContentMode.Structured)
{
var buffer = formatter.EncodeStructuredEvent(cloudEvent, out var contentType, cloudEvent.Extensions.Values);
var buffer =
formatter.EncodeStructuredEvent(cloudEvent, out var contentType, cloudEvent.Extensions.Values);
httpListenerResponse.ContentType = contentType.ToString();
MapAttributesToListenerResponse(cloudEvent, httpListenerResponse);
return httpListenerResponse.OutputStream.WriteAsync(buffer, 0, buffer.Length);
@ -39,34 +40,13 @@ namespace CloudNative.CloudEvents
return stream.CopyToAsync(httpListenerResponse.OutputStream);
}
private static Stream MapDataAttributeToStream(CloudEvent cloudEvent, ICloudEventFormatter formatter)
{
Stream stream;
if (cloudEvent.Data is byte[])
{
stream = new MemoryStream((byte[])cloudEvent.Data);
}
else if (cloudEvent.Data is string)
{
stream = new MemoryStream(Encoding.UTF8.GetBytes((string)cloudEvent.Data));
}
else if (cloudEvent.Data is Stream)
{
stream = (Stream)cloudEvent.Data;
}
else
{
stream = new MemoryStream(formatter.EncodeAttribute(CloudEventAttributes.DataAttributeName, cloudEvent.Data, cloudEvent.Extensions.Values));
}
return stream;
}
public static async Task CopyFromAsync(this HttpWebRequest httpWebRequest, CloudEvent cloudEvent, ContentMode contentMode, ICloudEventFormatter formatter)
public static async Task CopyFromAsync(this HttpWebRequest httpWebRequest, CloudEvent cloudEvent,
ContentMode contentMode, ICloudEventFormatter formatter)
{
if (contentMode == ContentMode.Structured)
{
var buffer = formatter.EncodeStructuredEvent(cloudEvent, out var contentType, cloudEvent.Extensions.Values);
var buffer =
formatter.EncodeStructuredEvent(cloudEvent, out var contentType, cloudEvent.Extensions.Values);
httpWebRequest.ContentType = contentType.ToString();
MapAttributesToWebRequest(cloudEvent, httpWebRequest);
await (httpWebRequest.GetRequestStream()).WriteAsync(buffer, 0, buffer.Length);
@ -79,68 +59,6 @@ namespace CloudNative.CloudEvents
await stream.CopyToAsync(httpWebRequest.GetRequestStream());
}
static void MapAttributesToListenerResponse(CloudEvent cloudEvent, HttpListenerResponse httpListenerResponse)
{
foreach (var attribute in cloudEvent.GetAttributes())
{
switch (attribute.Key)
{
case CloudEventAttributes.DataAttributeName:
case CloudEventAttributes.ContentTypeAttributeName:
break;
default:
if (attribute.Value is string)
{
httpListenerResponse.Headers.Add(HttpHeaderPrefix + attribute.Key, attribute.Value.ToString());
}
else if (attribute.Value is DateTime)
{
httpListenerResponse.Headers.Add(HttpHeaderPrefix + attribute.Key, ((DateTime)attribute.Value).ToString("o"));
}
else if (attribute.Value is Uri || attribute.Value is int)
{
httpListenerResponse.Headers.Add(HttpHeaderPrefix + attribute.Key, attribute.Value.ToString());
}
else
{
httpListenerResponse.Headers.Add(HttpHeaderPrefix + attribute.Key, Encoding.UTF8.GetString(jsonFormatter.EncodeAttribute(attribute.Key, attribute.Value, cloudEvent.Extensions.Values)));
}
break;
}
}
}
static void MapAttributesToWebRequest(CloudEvent cloudEvent, HttpWebRequest httpWebRequest)
{
foreach (var attribute in cloudEvent.GetAttributes())
{
switch (attribute.Key)
{
case CloudEventAttributes.DataAttributeName:
case CloudEventAttributes.ContentTypeAttributeName:
break;
default:
if (attribute.Value is string)
{
httpWebRequest.Headers.Add(HttpHeaderPrefix + attribute.Key, attribute.Value.ToString());
}
else if (attribute.Value is DateTime)
{
httpWebRequest.Headers.Add(HttpHeaderPrefix + attribute.Key, ((DateTime)attribute.Value).ToString("o"));
}
else if (attribute.Value is Uri || attribute.Value is int)
{
httpWebRequest.Headers.Add(HttpHeaderPrefix + attribute.Key, attribute.Value.ToString());
}
else
{
httpWebRequest.Headers.Add(HttpHeaderPrefix + attribute.Key, Encoding.UTF8.GetString(jsonFormatter.EncodeAttribute(attribute.Key, attribute.Value, cloudEvent.Extensions.Values)));
}
break;
}
}
}
public static bool HasCloudEvent(this HttpResponseMessage httpResponseMessage)
{
return ((httpResponseMessage.Content.Headers.ContentType != null &&
@ -160,19 +78,26 @@ namespace CloudNative.CloudEvents
return ToCloudEventInternalAsync(httpResponseMessage, formatter, extensions);
}
public static Task<CloudEvent> ToCloudEventAsync(this HttpListenerRequest httpListenerRequest,
params ICloudEventExtension[] extensions)
{
return ToCloudEventAsync(httpListenerRequest, null, extensions);
}
public static async Task<CloudEvent> ToCloudEventAsync(this HttpListenerRequest httpListenerRequest,
ICloudEventFormatter formatter,
ICloudEventFormatter formatter = null,
params ICloudEventExtension[] extensions)
{
if (httpListenerRequest.ContentType != null &&
httpListenerRequest.ContentType.StartsWith(CloudEvent.MediaType,
StringComparison.InvariantCultureIgnoreCase))
httpListenerRequest.ContentType.StartsWith(CloudEvent.MediaType,
StringComparison.InvariantCultureIgnoreCase))
{
// handle structured mode
if (formatter == null)
{
// if we didn't get a formatter, pick one
if (httpListenerRequest.ContentType.EndsWith(JsonEventFormatter.MediaTypeSuffix, StringComparison.InvariantCultureIgnoreCase))
if (httpListenerRequest.ContentType.EndsWith(JsonEventFormatter.MediaTypeSuffix,
StringComparison.InvariantCultureIgnoreCase))
{
formatter = jsonFormatter;
}
@ -193,7 +118,8 @@ namespace CloudNative.CloudEvents
if (httpResponseHeader.StartsWith(HttpHeaderPrefix, StringComparison.InvariantCultureIgnoreCase))
{
string headerValue = httpListenerRequest.Headers[httpResponseHeader];
if (headerValue.StartsWith("{") && headerValue.EndsWith("}") || headerValue.StartsWith("[") && headerValue.EndsWith("]"))
if (headerValue.StartsWith("{") && headerValue.EndsWith("}") ||
headerValue.StartsWith("[") && headerValue.EndsWith("]"))
{
attributes[httpResponseHeader.Substring(3).ToLowerInvariant()] =
JsonConvert.DeserializeObject(headerValue);
@ -213,6 +139,101 @@ namespace CloudNative.CloudEvents
}
}
static void MapAttributesToListenerResponse(CloudEvent cloudEvent, HttpListenerResponse httpListenerResponse)
{
foreach (var attribute in cloudEvent.GetAttributes())
{
switch (attribute.Key)
{
case CloudEventAttributes.DataAttributeName:
case CloudEventAttributes.ContentTypeAttributeName:
break;
default:
if (attribute.Value is string)
{
httpListenerResponse.Headers.Add(HttpHeaderPrefix + attribute.Key,
attribute.Value.ToString());
}
else if (attribute.Value is DateTime)
{
httpListenerResponse.Headers.Add(HttpHeaderPrefix + attribute.Key,
((DateTime)attribute.Value).ToString("o"));
}
else if (attribute.Value is Uri || attribute.Value is int)
{
httpListenerResponse.Headers.Add(HttpHeaderPrefix + attribute.Key,
attribute.Value.ToString());
}
else
{
httpListenerResponse.Headers.Add(HttpHeaderPrefix + attribute.Key,
Encoding.UTF8.GetString(jsonFormatter.EncodeAttribute(attribute.Key, attribute.Value,
cloudEvent.Extensions.Values)));
}
break;
}
}
}
static void MapAttributesToWebRequest(CloudEvent cloudEvent, HttpWebRequest httpWebRequest)
{
foreach (var attribute in cloudEvent.GetAttributes())
{
switch (attribute.Key)
{
case CloudEventAttributes.DataAttributeName:
case CloudEventAttributes.ContentTypeAttributeName:
break;
default:
if (attribute.Value is string)
{
httpWebRequest.Headers.Add(HttpHeaderPrefix + attribute.Key, attribute.Value.ToString());
}
else if (attribute.Value is DateTime)
{
httpWebRequest.Headers.Add(HttpHeaderPrefix + attribute.Key,
((DateTime)attribute.Value).ToString("o"));
}
else if (attribute.Value is Uri || attribute.Value is int)
{
httpWebRequest.Headers.Add(HttpHeaderPrefix + attribute.Key, attribute.Value.ToString());
}
else
{
httpWebRequest.Headers.Add(HttpHeaderPrefix + attribute.Key,
Encoding.UTF8.GetString(jsonFormatter.EncodeAttribute(attribute.Key, attribute.Value,
cloudEvent.Extensions.Values)));
}
break;
}
}
}
static Stream MapDataAttributeToStream(CloudEvent cloudEvent, ICloudEventFormatter formatter)
{
Stream stream;
if (cloudEvent.Data is byte[])
{
stream = new MemoryStream((byte[])cloudEvent.Data);
}
else if (cloudEvent.Data is string)
{
stream = new MemoryStream(Encoding.UTF8.GetBytes((string)cloudEvent.Data));
}
else if (cloudEvent.Data is Stream)
{
stream = (Stream)cloudEvent.Data;
}
else
{
stream = new MemoryStream(formatter.EncodeAttribute(CloudEventAttributes.DataAttributeName,
cloudEvent.Data, cloudEvent.Extensions.Values));
}
return stream;
}
static async Task<CloudEvent> ToCloudEventInternalAsync(HttpResponseMessage httpResponseMessage,
ICloudEventFormatter formatter, ICloudEventExtension[] extensions)
@ -245,10 +266,12 @@ namespace CloudNative.CloudEvents
var attributes = cloudEvent.GetAttributes();
foreach (var httpResponseHeader in httpResponseMessage.Headers)
{
if (httpResponseHeader.Key.StartsWith(HttpHeaderPrefix, StringComparison.InvariantCultureIgnoreCase))
if (httpResponseHeader.Key.StartsWith(HttpHeaderPrefix,
StringComparison.InvariantCultureIgnoreCase))
{
string headerValue = httpResponseHeader.Value.First();
if (headerValue.StartsWith("{") && headerValue.EndsWith("}") || headerValue.StartsWith("[") && headerValue.EndsWith("]"))
if (headerValue.StartsWith("{") && headerValue.EndsWith("}") ||
headerValue.StartsWith("[") && headerValue.EndsWith("]"))
{
attributes[httpResponseHeader.Key.Substring(3).ToLowerInvariant()] =
JsonConvert.DeserializeObject(headerValue);

View File

@ -26,7 +26,7 @@ namespace CloudNative.CloudEvents.UnitTests
attrs["comexampleextension1"] = "value";
attrs["comexampleextension2"] = new { othervalue = 5 };
Assert.Equal("0.1", cloudEvent.SpecVersion);
Assert.Equal("0.2", cloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", cloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), cloudEvent.Source);
Assert.Equal("A234-1234-1234", cloudEvent.Id);
@ -57,7 +57,7 @@ namespace CloudNative.CloudEvents.UnitTests
attrs["comexampleextension1"] = "value";
attrs["comexampleextension2"] = new { othervalue = 5 };
Assert.Equal("0.1", cloudEvent.SpecVersion);
Assert.Equal("0.2", cloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", cloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), cloudEvent.Source);
Assert.Equal("A234-1234-1234", cloudEvent.Id);
@ -95,7 +95,7 @@ namespace CloudNative.CloudEvents.UnitTests
Data = "<much wow=\"xml\"/>"
};
Assert.Equal("0.1", cloudEvent.SpecVersion);
Assert.Equal("0.2", cloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", cloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), cloudEvent.Source);
Assert.Equal("A234-1234-1234", cloudEvent.Id);

View File

@ -6,21 +6,23 @@ namespace CloudNative.CloudEvents.UnitTests
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Security.Authentication.ExtendedProtection;
using System.Threading.Tasks;
using Xunit;
public class HttpTest : IDisposable
{
private const string listenerAddress = "http://localhost:52671/";
private const string testContextHeader = "testcontext";
const string listenerAddress = "http://localhost:52671/";
const string testContextHeader = "testcontext";
HttpListener listener;
ConcurrentDictionary<string, Func<HttpListenerContext, Task>> pendingRequests = new ConcurrentDictionary<string, Func<HttpListenerContext, Task>>();
ConcurrentDictionary<string, Func<HttpListenerContext, Task>> pendingRequests =
new ConcurrentDictionary<string, Func<HttpListenerContext, Task>>();
public HttpTest()
{
@ -37,7 +39,11 @@ namespace CloudNative.CloudEvents.UnitTests
HandleContext(t.Result);
}
});
}
public void Dispose()
{
listener.Stop();
}
async Task HandleContext(HttpListenerContext requestContext)
@ -58,52 +64,29 @@ namespace CloudNative.CloudEvents.UnitTests
#pragma warning restore 4014
}
public void Dispose()
{
listener.Stop();
}
[Fact]
async Task HttpStructuredClientSendTest()
async Task HttpBinaryClientReceiveTest()
{
var cloudEvent = new CloudEvent("com.github.pull.create",
new Uri("https://github.com/cloudevents/spec/pull/123"))
{
Id = "A234-1234-1234",
Time = new DateTime(2018, 4, 5, 17, 31, 0, DateTimeKind.Utc),
ContentType = new ContentType(MediaTypeNames.Text.Xml),
Data = "<much wow=\"xml\"/>"
};
var attrs = cloudEvent.GetAttributes();
attrs["comexampleextension1"] = "value";
attrs["comexampleextension2"] = new { othervalue = 5 };
string ctx = Guid.NewGuid().ToString();
var content = new CloudEventContent(cloudEvent, ContentMode.Structured, new JsonEventFormatter());
content.Headers.Add(testContextHeader, ctx);
pendingRequests.TryAdd(ctx, async context =>
{
try
{
var receivedCloudEvent = await context.Request.ToCloudEventAsync(new JsonEventFormatter());
var cloudEvent = new CloudEvent("com.github.pull.create",
new Uri("https://github.com/cloudevents/spec/pull/123"))
{
Id = "A234-1234-1234",
Time = new DateTime(2018, 4, 5, 17, 31, 0, DateTimeKind.Utc),
ContentType = new ContentType(MediaTypeNames.Text.Xml),
Data = "<much wow=\"xml\"/>"
};
Assert.Equal("0.1", receivedCloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", receivedCloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), receivedCloudEvent.Source);
Assert.Equal("A234-1234-1234", receivedCloudEvent.Id);
Assert.Equal(DateTime.Parse("2018-04-05T17:31:00Z").ToUniversalTime(),
receivedCloudEvent.Time.Value.ToUniversalTime());
Assert.Equal(new ContentType(MediaTypeNames.Text.Xml), receivedCloudEvent.ContentType);
Assert.Equal("<much wow=\"xml\"/>", receivedCloudEvent.Data);
var attrs = cloudEvent.GetAttributes();
attrs["comexampleextension1"] = "value";
attrs["comexampleextension2"] = new { othervalue = 5 };
var attr = receivedCloudEvent.GetAttributes();
Assert.Equal("value", (string)attr["comexampleextension1"]);
Assert.Equal(5, (int)((dynamic)attr["comexampleextension2"]).othervalue);
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
await context.Response.CopyFromAsync(cloudEvent, ContentMode.Binary, new JsonEventFormatter());
context.Response.StatusCode = (int)HttpStatusCode.OK;
}
catch (Exception e)
{
@ -112,23 +95,40 @@ namespace CloudNative.CloudEvents.UnitTests
sw.Write(e.ToString());
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
}
context.Response.Close();
});
var httpClient = new HttpClient();
var result = (await httpClient.PostAsync(new Uri(listenerAddress + "ep"), content));
if (result.StatusCode != HttpStatusCode.NoContent)
httpClient.DefaultRequestHeaders.Add(testContextHeader, ctx);
var result = await httpClient.GetAsync(new Uri(listenerAddress + "ep"));
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
var receivedCloudEvent = await result.ToCloudEvent();
Assert.Equal("0.2", receivedCloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", receivedCloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), receivedCloudEvent.Source);
Assert.Equal("A234-1234-1234", receivedCloudEvent.Id);
Assert.Equal(DateTime.Parse("2018-04-05T17:31:00Z").ToUniversalTime(),
receivedCloudEvent.Time.Value.ToUniversalTime());
Assert.Equal(new ContentType(MediaTypeNames.Text.Xml), receivedCloudEvent.ContentType);
using (var sr = new StreamReader((Stream)receivedCloudEvent.Data))
{
throw new InvalidOperationException(result.Content.ReadAsStringAsync().GetAwaiter().GetResult());
Assert.Equal("<much wow=\"xml\"/>", sr.ReadToEnd());
}
var attr = receivedCloudEvent.GetAttributes();
Assert.Equal("value", (string)attr["comexampleextension1"]);
Assert.Equal(5, (int)((dynamic)attr["comexampleextension2"]).othervalue);
}
[Fact]
async Task HttpBinaryClientSendTest()
{
var cloudEvent = new CloudEvent("com.github.pull.create",
new Uri("https://github.com/cloudevents/spec/pull/123"))
new Uri("https://github.com/cloudevents/spec/pull/123"))
{
Id = "A234-1234-1234",
Time = new DateTime(2018, 4, 5, 17, 31, 0, DateTimeKind.Utc),
@ -140,19 +140,17 @@ namespace CloudNative.CloudEvents.UnitTests
attrs["comexampleextension1"] = "value";
attrs["comexampleextension2"] = new { othervalue = 5 };
string ctx = Guid.NewGuid().ToString();
var content = new CloudEventContent(cloudEvent, ContentMode.Binary, new JsonEventFormatter());
content.Headers.Add(testContextHeader, ctx);
pendingRequests.TryAdd(ctx, async context =>
{
try
{
var receivedCloudEvent = await context.Request.ToCloudEventAsync(new JsonEventFormatter());
Assert.Equal("0.1", receivedCloudEvent.SpecVersion);
Assert.Equal("0.2", receivedCloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", receivedCloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), receivedCloudEvent.Source);
Assert.Equal("A234-1234-1234", receivedCloudEvent.Id);
@ -178,6 +176,7 @@ namespace CloudNative.CloudEvents.UnitTests
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
context.Response.Close();
});
@ -189,14 +188,12 @@ namespace CloudNative.CloudEvents.UnitTests
}
}
[Fact]
async Task HttpStructuredClientReceiveTest()
{
string ctx = Guid.NewGuid().ToString();
pendingRequests.TryAdd(ctx, async context =>
{
try
{
var cloudEvent = new CloudEvent("com.github.pull.create",
@ -212,7 +209,6 @@ namespace CloudNative.CloudEvents.UnitTests
attrs["comexampleextension1"] = "value";
attrs["comexampleextension2"] = new { othervalue = 5 };
await context.Response.CopyFromAsync(cloudEvent, ContentMode.Structured, new JsonEventFormatter());
context.Response.StatusCode = (int)HttpStatusCode.OK;
}
@ -224,66 +220,7 @@ namespace CloudNative.CloudEvents.UnitTests
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
context.Response.Close();
});
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add(testContextHeader, ctx);
var result = await httpClient.GetAsync(new Uri(listenerAddress + "ep"));
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
var receivedCloudEvent = await result.ToCloudEvent();
Assert.Equal("0.1", receivedCloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", receivedCloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), receivedCloudEvent.Source);
Assert.Equal("A234-1234-1234", receivedCloudEvent.Id);
Assert.Equal(DateTime.Parse("2018-04-05T17:31:00Z").ToUniversalTime(),
receivedCloudEvent.Time.Value.ToUniversalTime());
Assert.Equal(new ContentType(MediaTypeNames.Text.Xml), receivedCloudEvent.ContentType);
Assert.Equal("<much wow=\"xml\"/>", receivedCloudEvent.Data);
var attr = receivedCloudEvent.GetAttributes();
Assert.Equal("value", (string)attr["comexampleextension1"]);
Assert.Equal(5, (int)((dynamic)attr["comexampleextension2"]).othervalue);
}
[Fact]
async Task HttpBinaryClientReceiveTest()
{
string ctx = Guid.NewGuid().ToString();
pendingRequests.TryAdd(ctx, async context =>
{
try
{
var cloudEvent = new CloudEvent("com.github.pull.create",
new Uri("https://github.com/cloudevents/spec/pull/123"))
{
Id = "A234-1234-1234",
Time = new DateTime(2018, 4, 5, 17, 31, 0, DateTimeKind.Utc),
ContentType = new ContentType(MediaTypeNames.Text.Xml),
Data = "<much wow=\"xml\"/>"
};
var attrs = cloudEvent.GetAttributes();
attrs["comexampleextension1"] = "value";
attrs["comexampleextension2"] = new { othervalue = 5 };
await context.Response.CopyFromAsync(cloudEvent, ContentMode.Binary, new JsonEventFormatter());
context.Response.StatusCode = (int)HttpStatusCode.OK;
}
catch (Exception e)
{
using (var sw = new StreamWriter(context.Response.OutputStream))
{
sw.Write(e.ToString());
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
context.Response.Close();
});
@ -294,22 +231,139 @@ namespace CloudNative.CloudEvents.UnitTests
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
var receivedCloudEvent = await result.ToCloudEvent();
Assert.Equal("0.1", receivedCloudEvent.SpecVersion);
Assert.Equal("0.2", receivedCloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", receivedCloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), receivedCloudEvent.Source);
Assert.Equal("A234-1234-1234", receivedCloudEvent.Id);
Assert.Equal(DateTime.Parse("2018-04-05T17:31:00Z").ToUniversalTime(),
receivedCloudEvent.Time.Value.ToUniversalTime());
Assert.Equal(new ContentType(MediaTypeNames.Text.Xml), receivedCloudEvent.ContentType);
using (var sr = new StreamReader((Stream)receivedCloudEvent.Data))
{
Assert.Equal("<much wow=\"xml\"/>", sr.ReadToEnd());
}
Assert.Equal("<much wow=\"xml\"/>", receivedCloudEvent.Data);
var attr = receivedCloudEvent.GetAttributes();
Assert.Equal("value", (string)attr["comexampleextension1"]);
Assert.Equal(5, (int)((dynamic)attr["comexampleextension2"]).othervalue);
}
[Fact]
async Task HttpStructuredClientSendTest()
{
var cloudEvent = new CloudEvent("com.github.pull.create",
new Uri("https://github.com/cloudevents/spec/pull/123"))
{
Id = "A234-1234-1234",
Time = new DateTime(2018, 4, 5, 17, 31, 0, DateTimeKind.Utc),
ContentType = new ContentType(MediaTypeNames.Text.Xml),
Data = "<much wow=\"xml\"/>"
};
var attrs = cloudEvent.GetAttributes();
attrs["comexampleextension1"] = "value";
attrs["comexampleextension2"] = new { othervalue = 5 };
string ctx = Guid.NewGuid().ToString();
var content = new CloudEventContent(cloudEvent, ContentMode.Structured, new JsonEventFormatter());
content.Headers.Add(testContextHeader, ctx);
pendingRequests.TryAdd(ctx, async context =>
{
try
{
var receivedCloudEvent = await context.Request.ToCloudEventAsync(new JsonEventFormatter());
Assert.Equal("0.2", receivedCloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", receivedCloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), receivedCloudEvent.Source);
Assert.Equal("A234-1234-1234", receivedCloudEvent.Id);
Assert.Equal(DateTime.Parse("2018-04-05T17:31:00Z").ToUniversalTime(),
receivedCloudEvent.Time.Value.ToUniversalTime());
Assert.Equal(new ContentType(MediaTypeNames.Text.Xml), receivedCloudEvent.ContentType);
Assert.Equal("<much wow=\"xml\"/>", receivedCloudEvent.Data);
var attr = receivedCloudEvent.GetAttributes();
Assert.Equal("value", (string)attr["comexampleextension1"]);
Assert.Equal(5, (int)((dynamic)attr["comexampleextension2"]).othervalue);
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception e)
{
using (var sw = new StreamWriter(context.Response.OutputStream))
{
sw.Write(e.ToString());
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
context.Response.Close();
});
var httpClient = new HttpClient();
var result = (await httpClient.PostAsync(new Uri(listenerAddress + "ep"), content));
if (result.StatusCode != HttpStatusCode.NoContent)
{
throw new InvalidOperationException(result.Content.ReadAsStringAsync().GetAwaiter().GetResult());
}
}
[Fact]
async Task HttpStructuredWebRequestSendTest()
{
var cloudEvent = new CloudEvent("com.github.pull.create",
new Uri("https://github.com/cloudevents/spec/pull/123"))
{
Id = "A234-1234-1234",
Time = new DateTime(2018, 4, 5, 17, 31, 0, DateTimeKind.Utc),
ContentType = new ContentType(MediaTypeNames.Text.Xml),
Data = "<much wow=\"xml\"/>"
};
var attrs = cloudEvent.GetAttributes();
attrs["comexampleextension1"] = "value";
attrs["comexampleextension2"] = new { othervalue = 5 };
string ctx = Guid.NewGuid().ToString();
HttpWebRequest httpWebRequest = WebRequest.CreateHttp(listenerAddress + "ep");
httpWebRequest.Method = "POST";
await httpWebRequest.CopyFromAsync(cloudEvent, ContentMode.Structured, new JsonEventFormatter());
httpWebRequest.Headers.Add(testContextHeader, ctx);
pendingRequests.TryAdd(ctx, async context =>
{
try
{
var receivedCloudEvent = await context.Request.ToCloudEventAsync(new JsonEventFormatter());
Assert.Equal("0.2", receivedCloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", receivedCloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), receivedCloudEvent.Source);
Assert.Equal("A234-1234-1234", receivedCloudEvent.Id);
Assert.Equal(DateTime.Parse("2018-04-05T17:31:00Z").ToUniversalTime(),
receivedCloudEvent.Time.Value.ToUniversalTime());
Assert.Equal(new ContentType(MediaTypeNames.Text.Xml), receivedCloudEvent.ContentType);
Assert.Equal("<much wow=\"xml\"/>", receivedCloudEvent.Data);
var attr = receivedCloudEvent.GetAttributes();
Assert.Equal("value", (string)attr["comexampleextension1"]);
Assert.Equal(5, (int)((dynamic)attr["comexampleextension2"]).othervalue);
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception e)
{
using (var sw = new StreamWriter(context.Response.OutputStream))
{
sw.Write(e.ToString());
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
context.Response.Close();
});
var result = (HttpWebResponse)await httpWebRequest.GetResponseAsync();
if (result.StatusCode != HttpStatusCode.NoContent)
{
throw new InvalidOperationException(result.StatusCode.ToString());
}
}
}
}

View File

@ -13,7 +13,7 @@ namespace CloudNative.CloudEvents.UnitTests
{
const string json =
"{\n" +
" \"specversion\" : \"0.1\",\n" +
" \"specversion\" : \"0.2\",\n" +
" \"type\" : \"com.github.pull.create\",\n" +
" \"source\" : \"https://github.com/cloudevents/spec/pull/123\",\n" +
" \"id\" : \"A234-1234-1234\",\n" +
@ -48,7 +48,7 @@ namespace CloudNative.CloudEvents.UnitTests
{
var jsonFormatter = new JsonEventFormatter();
var cloudEvent = jsonFormatter.DecodeStructuredEvent(Encoding.UTF8.GetBytes(json));
Assert.Equal("0.1", cloudEvent.SpecVersion);
Assert.Equal("0.2", cloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", cloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), cloudEvent.Source);
Assert.Equal("A234-1234-1234", cloudEvent.Id);
@ -68,7 +68,7 @@ namespace CloudNative.CloudEvents.UnitTests
var jsonFormatter = new JsonEventFormatter();
var cloudEvent = jsonFormatter.DecodeStructuredEvent(Encoding.UTF8.GetBytes(json), new ComExampleExtension1Extension(),
new ComExampleExtension2Extension());
Assert.Equal("0.1", cloudEvent.SpecVersion);
Assert.Equal("0.2", cloudEvent.SpecVersion);
Assert.Equal("com.github.pull.create", cloudEvent.Type);
Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), cloudEvent.Source);
Assert.Equal("A234-1234-1234", cloudEvent.Id);