714 lines
30 KiB
C#
714 lines
30 KiB
C#
// Copyright 2022 Cloud Native Foundation.
|
|
// Licensed under the Apache 2.0 license.
|
|
// See LICENSE file in the project root for full license information.
|
|
|
|
using Google.Protobuf;
|
|
using Google.Protobuf.Collections;
|
|
using Google.Protobuf.WellKnownTypes;
|
|
using System;
|
|
using System.IO;
|
|
using System.Net.Mime;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Xunit;
|
|
using static CloudNative.CloudEvents.UnitTests.TestHelpers;
|
|
using static CloudNative.CloudEvents.V1.CloudEvent.Types;
|
|
|
|
namespace CloudNative.CloudEvents.Protobuf.UnitTests
|
|
{
|
|
/// <summary>
|
|
/// Tests for ProtobufEventFormatter. Note that most tests for encoding/decoding of
|
|
/// structured mode events (and batches) are performed via the public methods that
|
|
/// perform proto/CloudEvent conversions - the regular event formatter methods are
|
|
/// only wrappers around those, covered by minimal tests here.
|
|
/// </summary>
|
|
public class ProtobufEventFormatterTest
|
|
{
|
|
private static readonly ContentType s_protobufCloudEventContentType = new ContentType("application/cloudevents+protobuf");
|
|
private static readonly ContentType s_protobufCloudEventBatchContentType = new ContentType("application/cloudevents-batch+protobuf");
|
|
|
|
[Fact]
|
|
public void EncodeStructuredModeMessage_Minimal()
|
|
{
|
|
var cloudEvent = new CloudEvent(CloudEventsSpecVersion.V1_0)
|
|
{
|
|
Id = "event-id",
|
|
Source = new Uri("https://event-source"),
|
|
Type = "event-type",
|
|
};
|
|
|
|
var encoded = new ProtobufEventFormatter().EncodeStructuredModeMessage(cloudEvent, out var contentType);
|
|
Assert.Equal("application/cloudevents+protobuf; charset=utf-8", contentType.ToString());
|
|
var actualProto = V1.CloudEvent.Parser.ParseFrom(encoded.ToArray());
|
|
|
|
var expectedProto = new V1.CloudEvent
|
|
{
|
|
SpecVersion = "1.0",
|
|
Id = "event-id",
|
|
Source = "https://event-source",
|
|
Type = "event-type"
|
|
};
|
|
Assert.Equal(expectedProto, actualProto);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A simple test that populates all known v1.0 attributes, so we don't need to test that
|
|
/// aspect in the future.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ConvertToProto_V1Attributes()
|
|
{
|
|
var cloudEvent = new CloudEvent(CloudEventsSpecVersion.V1_0)
|
|
{
|
|
Data = "text",
|
|
DataContentType = "text/plain",
|
|
DataSchema = new Uri("https://data-schema"),
|
|
Id = "event-id",
|
|
Source = new Uri("https://event-source"),
|
|
Subject = "event-subject",
|
|
Time = new DateTimeOffset(2021, 2, 19, 12, 34, 56, 789, TimeSpan.FromHours(1)),
|
|
Type = "event-type"
|
|
};
|
|
|
|
var actualProto = new ProtobufEventFormatter().ConvertToProto(cloudEvent);
|
|
var expectedProto = new V1.CloudEvent
|
|
{
|
|
SpecVersion = "1.0",
|
|
Id = "event-id",
|
|
Source = "https://event-source",
|
|
Type = "event-type",
|
|
Attributes =
|
|
{
|
|
{ "datacontenttype", StringAttribute("text/plain") },
|
|
{ "dataschema", UriAttribute("https://data-schema") },
|
|
{ "subject", StringAttribute("event-subject") },
|
|
// Deliberately not reusing cloudEvent.Time: this demonstrates that only the instant in time
|
|
// is relevant, not the UTC offset.
|
|
{ "time", TimestampAttribute(new DateTimeOffset(2021, 2, 19, 11, 34, 56, 789, TimeSpan.Zero)) }
|
|
},
|
|
TextData = "text"
|
|
};
|
|
Assert.Equal(expectedProto, actualProto);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertToProto_AllAttributeTypes()
|
|
{
|
|
var cloudEvent = new CloudEvent(AllTypesExtensions)
|
|
{
|
|
["binary"] = SampleBinaryData,
|
|
["boolean"] = true,
|
|
["integer"] = 10,
|
|
["string"] = "text",
|
|
["timestamp"] = SampleTimestamp,
|
|
["uri"] = SampleUri,
|
|
["urireference"] = SampleUriReference
|
|
};
|
|
// We're not going to check these.
|
|
cloudEvent.PopulateRequiredAttributes();
|
|
|
|
var proto = new ProtobufEventFormatter().ConvertToProto(cloudEvent);
|
|
|
|
var expectedAttributes = new MapField<string, CloudEventAttributeValue>
|
|
{
|
|
{ "binary", BinaryAttribute(SampleBinaryData) },
|
|
{ "boolean", BooleanAttribute(true) },
|
|
{ "integer", IntegerAttribute(10) },
|
|
{ "string", StringAttribute("text") },
|
|
{ "timestamp", TimestampAttribute(SampleTimestamp) },
|
|
{ "uri", UriAttribute(SampleUriText) },
|
|
{ "urireference", UriRefAttribute(SampleUriReferenceText) }
|
|
};
|
|
Assert.Equal(proto.Attributes, expectedAttributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertToProto_NoData()
|
|
{
|
|
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
|
|
var proto = new ProtobufEventFormatter().ConvertToProto(cloudEvent);
|
|
Assert.Equal(V1.CloudEvent.DataOneofCase.None, proto.DataCase);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertToProto_TextData()
|
|
{
|
|
var cloudEvent = new CloudEvent { Data = "text" }.PopulateRequiredAttributes();
|
|
var proto = new ProtobufEventFormatter().ConvertToProto(cloudEvent);
|
|
Assert.Equal("text", proto.TextData);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertToProto_BinaryData()
|
|
{
|
|
var cloudEvent = new CloudEvent { Data = SampleBinaryData }.PopulateRequiredAttributes();
|
|
var proto = new ProtobufEventFormatter().ConvertToProto(cloudEvent);
|
|
Assert.Equal(SampleBinaryData, proto.BinaryData.ToByteArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertToProto_MessageData()
|
|
{
|
|
var data = new PayloadData1 { Name = "test" };
|
|
var cloudEvent = new CloudEvent { Data = data }.PopulateRequiredAttributes();
|
|
var proto = new ProtobufEventFormatter().ConvertToProto(cloudEvent);
|
|
Assert.Equal(Any.Pack(data), proto.ProtoData);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertToProto_MessageData_AlreadyPacked()
|
|
{
|
|
var data = new PayloadData1 { Name = "test" };
|
|
var packedData = Any.Pack(data);
|
|
var cloudEvent = new CloudEvent { Data = packedData }.PopulateRequiredAttributes();
|
|
var proto = new ProtobufEventFormatter().ConvertToProto(cloudEvent);
|
|
// This verifies that the formatter doesn't "double-encode".
|
|
Assert.Equal(packedData, proto.ProtoData);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertToProto_MessageData_CustomTypeUrlPrefix()
|
|
{
|
|
string typeUrlPrefix = "cloudevents.io/xyz";
|
|
var data = new PayloadData1 { Name = "test" };
|
|
var cloudEvent = new CloudEvent { Data = data }.PopulateRequiredAttributes();
|
|
var proto = new ProtobufEventFormatter(typeUrlPrefix).ConvertToProto(cloudEvent);
|
|
Assert.Equal(Any.Pack(data, typeUrlPrefix), proto.ProtoData);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertToProto_InvalidData()
|
|
{
|
|
var cloudEvent = new CloudEvent { Data = new object() }.PopulateRequiredAttributes();
|
|
var formatter = new ProtobufEventFormatter();
|
|
Assert.Throws<ArgumentException>(() => formatter.ConvertToProto(cloudEvent));
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeBinaryModeData_Bytes()
|
|
{
|
|
var cloudEvent = new CloudEvent
|
|
{
|
|
Data = SampleBinaryData
|
|
}.PopulateRequiredAttributes();
|
|
var formatter = new ProtobufEventFormatter();
|
|
|
|
var result = formatter.EncodeBinaryModeEventData(cloudEvent);
|
|
Assert.Equal(SampleBinaryData, result.ToArray());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("utf-8")]
|
|
[InlineData("iso-8859-1")]
|
|
[InlineData(null)]
|
|
public void EncodeBinaryModeData_String_TextContentType(string charset)
|
|
{
|
|
string text = "caf\u00e9"; // Valid in both UTF-8 and ISO-8859-1, but with different representations
|
|
var encoding = charset is null ? Encoding.UTF8 : Encoding.GetEncoding(charset);
|
|
string contentType = charset is null ? "text/plain" : $"text/plain; charset={charset}";
|
|
var cloudEvent = new CloudEvent
|
|
{
|
|
Data = text,
|
|
DataContentType = contentType
|
|
}.PopulateRequiredAttributes();
|
|
|
|
var formatter = new ProtobufEventFormatter();
|
|
var result = formatter.EncodeBinaryModeEventData(cloudEvent);
|
|
Assert.Equal(encoding.GetBytes(text), result.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeBinaryModeData_String_NonTextContentType()
|
|
{
|
|
var cloudEvent = new CloudEvent
|
|
{
|
|
Data = "text",
|
|
DataContentType = "application/json"
|
|
}.PopulateRequiredAttributes();
|
|
var formatter = new ProtobufEventFormatter();
|
|
Assert.Throws<ArgumentException>(() => formatter.EncodeBinaryModeEventData(cloudEvent));
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeBinaryModeData_ProtoMessage()
|
|
{
|
|
var cloudEvent = new CloudEvent
|
|
{
|
|
Data = new PayloadData1 { Name = "fail" },
|
|
DataContentType = "application/protobuf"
|
|
}.PopulateRequiredAttributes();
|
|
var formatter = new ProtobufEventFormatter();
|
|
// See summary documentation for ProtobufEventFormatter for the reasoning for this
|
|
Assert.Throws<ArgumentException>(() => formatter.EncodeBinaryModeEventData(cloudEvent));
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeBinaryModeData_ArbitraryObject()
|
|
{
|
|
var cloudEvent = new CloudEvent
|
|
{
|
|
Data = new object(),
|
|
DataContentType = "application/octet-stream"
|
|
}.PopulateRequiredAttributes();
|
|
var formatter = new ProtobufEventFormatter();
|
|
// See summary documentation for ProtobufEventFormatter for the reasoning for this
|
|
Assert.Throws<ArgumentException>(() => formatter.EncodeBinaryModeEventData(cloudEvent));
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeBinaryModeData_NoData()
|
|
{
|
|
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
|
|
var formatter = new ProtobufEventFormatter();
|
|
Assert.Empty(formatter.EncodeBinaryModeEventData(cloudEvent).ToArray());
|
|
}
|
|
|
|
// Just a single test for the code that parses asynchronously... the guts are all the same.
|
|
[Fact]
|
|
public async Task DecodeStructuredModeMessageAsync_Minimal()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
byte[] bytes = proto.ToByteArray();
|
|
var stream = new MemoryStream(bytes);
|
|
var formatter = new ProtobufEventFormatter();
|
|
var cloudEvent = await formatter.DecodeStructuredModeMessageAsync(stream, s_protobufCloudEventContentType, null);
|
|
Assert.Equal("test-type", cloudEvent.Type);
|
|
Assert.Equal("test-id", cloudEvent.Id);
|
|
Assert.Equal(SampleUri, cloudEvent.Source);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodeStructuredModeMessage_Minimal()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
byte[] bytes = proto.ToByteArray();
|
|
var stream = new MemoryStream(bytes);
|
|
var formatter = new ProtobufEventFormatter();
|
|
var cloudEvent = formatter.DecodeStructuredModeMessage(stream, s_protobufCloudEventContentType, null);
|
|
Assert.Equal("test-type", cloudEvent.Type);
|
|
Assert.Equal("test-id", cloudEvent.Id);
|
|
Assert.Equal(SampleUri, cloudEvent.Source);
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeBatchModeMessage_Empty()
|
|
{
|
|
var formatter = new ProtobufEventFormatter();
|
|
var bytes = formatter.EncodeBatchModeMessage(new CloudEvent[0], out var contentType);
|
|
Assert.Equal("application/cloudevents-batch+protobuf; charset=utf-8", contentType.ToString());
|
|
var batch = V1.CloudEventBatch.Parser.ParseFrom(bytes.ToArray());
|
|
Assert.Empty(batch.Events);
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeBatchModeMessage_TwoEvents()
|
|
{
|
|
var event1 = new CloudEvent
|
|
{
|
|
Id = "event1",
|
|
Type = "type1",
|
|
Source = new Uri("//event-source1", UriKind.RelativeOrAbsolute),
|
|
Data = "simple text",
|
|
DataContentType = "text/plain"
|
|
};
|
|
var event2 = new CloudEvent
|
|
{
|
|
Id = "event2",
|
|
Type = "type2",
|
|
Source = new Uri("//event-source2", UriKind.RelativeOrAbsolute),
|
|
};
|
|
|
|
var cloudEvents = new[] { event1, event2 };
|
|
var formatter = new ProtobufEventFormatter();
|
|
var bytes = formatter.EncodeBatchModeMessage(cloudEvents, out var contentType);
|
|
Assert.Equal("application/cloudevents-batch+protobuf; charset=utf-8", contentType.ToString());
|
|
var actualBatch = V1.CloudEventBatch.Parser.ParseFrom(bytes.ToArray());
|
|
var expectedBatch = new V1.CloudEventBatch
|
|
{
|
|
Events =
|
|
{
|
|
new V1.CloudEvent
|
|
{
|
|
SpecVersion = "1.0",
|
|
Type = "type1",
|
|
Id = "event1",
|
|
Source = "//event-source1",
|
|
TextData = "simple text",
|
|
Attributes = { { "datacontenttype", StringAttribute("text/plain") } }
|
|
},
|
|
new V1.CloudEvent
|
|
{
|
|
SpecVersion = "1.0",
|
|
Type = "type2",
|
|
Id = "event2",
|
|
Source = "//event-source2"
|
|
}
|
|
}
|
|
};
|
|
Assert.Equal(expectedBatch, actualBatch);
|
|
}
|
|
|
|
[Fact]
|
|
public void EncodeBatchModeMessage_Invalid()
|
|
{
|
|
var formatter = new ProtobufEventFormatter();
|
|
// Invalid CloudEvent
|
|
Assert.Throws<ArgumentException>(() => formatter.EncodeBatchModeMessage(new[] { new CloudEvent() }, out _));
|
|
// Null argument
|
|
Assert.Throws<ArgumentNullException>(() => formatter.EncodeBatchModeMessage(null!, out _));
|
|
// Null value within the argument. Arguably this should throw ArgumentException instead of
|
|
// ArgumentNullException, but it's unlikely to cause confusion.
|
|
Assert.Throws<ArgumentNullException>(() => formatter.EncodeBatchModeMessage(new CloudEvent[1], out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_V1Attributes()
|
|
{
|
|
var proto = new V1.CloudEvent
|
|
{
|
|
SpecVersion = "1.0",
|
|
Type = "test-type",
|
|
Id = "test-id",
|
|
TextData = "text",
|
|
Source = "//event-source",
|
|
Attributes =
|
|
{
|
|
{ "datacontenttype", StringAttribute("text/plain") },
|
|
{ "dataschema", UriAttribute("https://data-schema") },
|
|
{ "subject", StringAttribute("event-subject") },
|
|
{ "time", TimestampAttribute(SampleTimestamp) }
|
|
}
|
|
};
|
|
var cloudEvent = new ProtobufEventFormatter().ConvertFromProto(proto, null);
|
|
Assert.Equal(CloudEventsSpecVersion.V1_0, cloudEvent.SpecVersion);
|
|
Assert.Equal("test-type", cloudEvent.Type);
|
|
Assert.Equal("test-id", cloudEvent.Id);
|
|
Assert.Equal("text/plain", cloudEvent.DataContentType);
|
|
Assert.Equal(new Uri("https://data-schema"), cloudEvent.DataSchema);
|
|
Assert.Equal("event-subject", cloudEvent.Subject);
|
|
Assert.Equal(new Uri("//event-source", UriKind.RelativeOrAbsolute), cloudEvent.Source);
|
|
// The protobuf timestamp loses the offset information, but is still the correct instant.
|
|
AssertTimestampsEqual(SampleTimestamp.ToUniversalTime(), cloudEvent.Time);
|
|
}
|
|
|
|
[Theory]
|
|
// These are required, so have to be specified in the dedicated protobuf field
|
|
[InlineData("id")]
|
|
[InlineData("type")]
|
|
[InlineData("source")]
|
|
// These are generally invalid attribute names
|
|
[InlineData("specversion")]
|
|
[InlineData("a b c")]
|
|
[InlineData("ABC")]
|
|
public void ConvertFromProto_InvalidAttributeNames(string attributeName)
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.Attributes.Add(attributeName, StringAttribute("value"));
|
|
var formatter = new ProtobufEventFormatter();
|
|
Assert.Throws<ArgumentException>(() => formatter.ConvertFromProto(proto, null));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_AllAttributeTypes()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.Attributes.Add("binary", BinaryAttribute(SampleBinaryData));
|
|
proto.Attributes.Add("boolean", BooleanAttribute(true));
|
|
proto.Attributes.Add("integer", IntegerAttribute(10));
|
|
proto.Attributes.Add("string", StringAttribute("text"));
|
|
proto.Attributes.Add("timestamp", TimestampAttribute(SampleTimestamp));
|
|
proto.Attributes.Add("uri", UriAttribute(SampleUriText));
|
|
proto.Attributes.Add("urireference", UriRefAttribute(SampleUriReferenceText));
|
|
|
|
var cloudEvent = new ProtobufEventFormatter().ConvertFromProto(proto, null);
|
|
Assert.Equal(SampleBinaryData, cloudEvent["binary"]);
|
|
Assert.True((bool) cloudEvent["boolean"]!);
|
|
Assert.Equal(10, cloudEvent["integer"]);
|
|
Assert.Equal("text", cloudEvent["string"]);
|
|
// The protobuf timestamp loses the offset information, but is still the correct instant.
|
|
AssertTimestampsEqual(SampleTimestamp.ToUniversalTime(), (DateTimeOffset) cloudEvent["timestamp"]!);
|
|
Assert.Equal(SampleUri, cloudEvent["uri"]);
|
|
Assert.Equal(SampleUriReference, cloudEvent["urireference"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_NoData()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
var cloudEvent = new ProtobufEventFormatter().ConvertFromProto(proto, null);
|
|
Assert.Null(cloudEvent.Data);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_TextData()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.TextData = "text";
|
|
var cloudEvent = new ProtobufEventFormatter().ConvertFromProto(proto, null);
|
|
Assert.Equal("text", cloudEvent.Data);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_BinaryData()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.BinaryData = ByteString.CopyFrom(SampleBinaryData);
|
|
var cloudEvent = new ProtobufEventFormatter().ConvertFromProto(proto, null);
|
|
Assert.Equal(SampleBinaryData, cloudEvent.Data);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_MessageData()
|
|
{
|
|
var message = new PayloadData1 { Name = "testing" };
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.ProtoData = Any.Pack(message);
|
|
var cloudEvent = new ProtobufEventFormatter().ConvertFromProto(proto, null);
|
|
// Note: this isn't unpacked automatically.
|
|
Assert.Equal(Any.Pack(message), cloudEvent.Data);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_UnspecifiedExtensionAttributes()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.Attributes.Add("xyz", StringAttribute("abc"));
|
|
var cloudEvent = new ProtobufEventFormatter().ConvertFromProto(proto, null);
|
|
Assert.Equal("abc", cloudEvent["xyz"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_SpecifiedExtensionAttributes_Valid()
|
|
{
|
|
var attribute = CloudEventAttribute.CreateExtension("xyz", CloudEventAttributeType.String);
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.Attributes.Add(attribute.Name, StringAttribute("abc"));
|
|
var cloudEvent = new ProtobufEventFormatter().ConvertFromProto(proto, new[] { attribute });
|
|
Assert.Equal("abc", cloudEvent[attribute]);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_SpecifiedExtensionAttributes_UnexpectedType()
|
|
{
|
|
var attribute = CloudEventAttribute.CreateExtension("xyz", CloudEventAttributeType.UriReference);
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.Attributes.Add(attribute.Name, UriAttribute("https://xyz"));
|
|
// Even though the value would be valid as a URI reference, we fail because
|
|
// the type in the proto message is not the same as the type we've specified in the method argument.
|
|
Assert.Throws<ArgumentException>(() => new ProtobufEventFormatter().ConvertFromProto(proto, new[] { attribute }));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_SpecifiedExtensionAttributes_InvalidValue()
|
|
{
|
|
var attribute = CloudEventAttribute.CreateExtension("xyz", CloudEventAttributeType.Integer, ValidateValue);
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.Attributes.Add(attribute.Name, IntegerAttribute(1000));
|
|
var exception = Assert.Throws<ArgumentException>(() => new ProtobufEventFormatter().ConvertFromProto(proto, new[] { attribute }));
|
|
Assert.Equal("Boom!", exception!.InnerException!.Message);
|
|
|
|
void ValidateValue(object value)
|
|
{
|
|
if ((int) value > 100)
|
|
{
|
|
throw new Exception("Boom!");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_Invalid_NoSpecVersion()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.SpecVersion = "";
|
|
var exception = Assert.Throws<ArgumentException>(() => new ProtobufEventFormatter().ConvertFromProto(proto, null));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_Invalid_NoType()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.SpecVersion = "";
|
|
var exception = Assert.Throws<ArgumentException>(() => new ProtobufEventFormatter().ConvertFromProto(proto, null));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_Invalid_NoId()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.Id = "";
|
|
var exception = Assert.Throws<ArgumentException>(() => new ProtobufEventFormatter().ConvertFromProto(proto, null));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertFromProto_Invalid_NoSource()
|
|
{
|
|
var proto = CreateMinimalCloudEventProto();
|
|
proto.Source = "";
|
|
var exception = Assert.Throws<ArgumentException>(() => new ProtobufEventFormatter().ConvertFromProto(proto, null));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("utf-8")]
|
|
[InlineData("iso-8859-1")]
|
|
[InlineData(null)]
|
|
public void DecodeBinaryModeEventData_Text(string charset)
|
|
{
|
|
string text = "caf\u00e9"; // Valid in both UTF-8 and ISO-8859-1, but with different representations
|
|
var encoding = charset is null ? Encoding.UTF8 : Encoding.GetEncoding(charset);
|
|
var bytes = encoding.GetBytes(text);
|
|
string contentType = charset is null ? "text/plain" : $"text/plain; charset={charset}";
|
|
var data = DecodeBinaryModeEventData(bytes, contentType);
|
|
string actualText = Assert.IsType<string>(data);
|
|
Assert.Equal(text, actualText);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("application/json")]
|
|
[InlineData(null)]
|
|
public void DecodeBinaryModeData_NonTextContentType(string contentType)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes("{}");
|
|
var data = DecodeBinaryModeEventData(bytes, contentType);
|
|
byte[] actualBytes = Assert.IsType<byte[]>(data);
|
|
Assert.Equal(bytes, actualBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodeBatchMode_Minimal()
|
|
{
|
|
var batchProto = new V1.CloudEventBatch
|
|
{
|
|
Events = { CreateMinimalCloudEventProto() }
|
|
};
|
|
byte[] bytes = batchProto.ToByteArray();
|
|
var stream = new MemoryStream(bytes);
|
|
var formatter = new ProtobufEventFormatter();
|
|
var cloudEvents = formatter.DecodeBatchModeMessage(stream, s_protobufCloudEventBatchContentType, null);
|
|
var cloudEvent = Assert.Single(cloudEvents);
|
|
Assert.Equal("test-type", cloudEvent.Type);
|
|
Assert.Equal("test-id", cloudEvent.Id);
|
|
Assert.Equal(SampleUri, cloudEvent.Source);
|
|
}
|
|
|
|
// Just a single test for the code that parses asynchronously... the guts are all the same.
|
|
[Fact]
|
|
public async Task DecodeBatchModeMessageAsync_Minimal()
|
|
{
|
|
var batchProto = new V1.CloudEventBatch
|
|
{
|
|
Events = { CreateMinimalCloudEventProto() }
|
|
};
|
|
byte[] bytes = batchProto.ToByteArray();
|
|
var stream = new MemoryStream(bytes);
|
|
var formatter = new ProtobufEventFormatter();
|
|
var cloudEvents = await formatter.DecodeBatchModeMessageAsync(stream, s_protobufCloudEventBatchContentType, null);
|
|
var cloudEvent = Assert.Single(cloudEvents);
|
|
Assert.Equal("test-type", cloudEvent.Type);
|
|
Assert.Equal("test-id", cloudEvent.Id);
|
|
Assert.Equal(SampleUri, cloudEvent.Source);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodeBatchMode_Empty()
|
|
{
|
|
var batchProto = new V1.CloudEventBatch();
|
|
byte[] bytes = batchProto.ToByteArray();
|
|
var stream = new MemoryStream(bytes);
|
|
var formatter = new ProtobufEventFormatter();
|
|
var cloudEvents = formatter.DecodeBatchModeMessage(stream, s_protobufCloudEventBatchContentType, null);
|
|
Assert.Empty(cloudEvents);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodeBatchMode_Multiple()
|
|
{
|
|
var batchProto = new V1.CloudEventBatch
|
|
{
|
|
Events =
|
|
{
|
|
new V1.CloudEvent
|
|
{
|
|
SpecVersion = "1.0",
|
|
Type = "type1",
|
|
Id = "event1",
|
|
Source = "//event-source1",
|
|
TextData = "simple text",
|
|
Attributes = { { "datacontenttype", StringAttribute("text/plain") } }
|
|
},
|
|
new V1.CloudEvent
|
|
{
|
|
SpecVersion = "1.0",
|
|
Type = "type2",
|
|
Id = "event2",
|
|
Source = "//event-source2"
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
byte[] bytes = batchProto.ToByteArray();
|
|
var stream = new MemoryStream(bytes);
|
|
var formatter = new ProtobufEventFormatter();
|
|
var cloudEvents = formatter.DecodeBatchModeMessage(stream, s_protobufCloudEventBatchContentType, null);
|
|
Assert.Equal(2, cloudEvents.Count);
|
|
|
|
var event1 = cloudEvents[0];
|
|
Assert.Equal("type1", event1.Type);
|
|
Assert.Equal("event1", event1.Id);
|
|
Assert.Equal(new Uri("//event-source1", UriKind.RelativeOrAbsolute), event1.Source);
|
|
Assert.Equal("simple text", event1.Data);
|
|
Assert.Equal("text/plain", event1.DataContentType);
|
|
|
|
var event2 = cloudEvents[1];
|
|
Assert.Equal("type2", event2.Type);
|
|
Assert.Equal("event2", event2.Id);
|
|
Assert.Equal(new Uri("//event-source2", UriKind.RelativeOrAbsolute), event2.Source);
|
|
Assert.Null(event2.Data);
|
|
Assert.Null(event2.DataContentType);
|
|
}
|
|
|
|
// Utility methods
|
|
|
|
private static object? DecodeBinaryModeEventData(byte[] bytes, string contentType)
|
|
{
|
|
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
|
|
cloudEvent.DataContentType = contentType;
|
|
new ProtobufEventFormatter().DecodeBinaryModeEventData(bytes, cloudEvent);
|
|
return cloudEvent.Data;
|
|
}
|
|
|
|
private static CloudEventAttributeValue StringAttribute(string value) =>
|
|
new CloudEventAttributeValue { CeString = value };
|
|
|
|
private static CloudEventAttributeValue BinaryAttribute(byte[] value) =>
|
|
new CloudEventAttributeValue { CeBytes = ByteString.CopyFrom(value) };
|
|
|
|
private static CloudEventAttributeValue BooleanAttribute(bool value) =>
|
|
new CloudEventAttributeValue { CeBoolean = value };
|
|
|
|
private static CloudEventAttributeValue IntegerAttribute(int value) =>
|
|
new CloudEventAttributeValue { CeInteger = value };
|
|
|
|
private static CloudEventAttributeValue UriAttribute(string value) =>
|
|
new CloudEventAttributeValue { CeUri = value };
|
|
|
|
private static CloudEventAttributeValue UriRefAttribute(string value) =>
|
|
new CloudEventAttributeValue { CeUriRef = value };
|
|
|
|
private static CloudEventAttributeValue TimestampAttribute(Timestamp value) =>
|
|
new CloudEventAttributeValue { CeTimestamp = value };
|
|
|
|
private static CloudEventAttributeValue TimestampAttribute(DateTimeOffset value) =>
|
|
TimestampAttribute(Timestamp.FromDateTimeOffset(value));
|
|
|
|
private static V1.CloudEvent CreateMinimalCloudEventProto() => new V1.CloudEvent
|
|
{
|
|
SpecVersion = "1.0",
|
|
Type = "test-type",
|
|
Id = "test-id",
|
|
Source = SampleUriText
|
|
};
|
|
}
|
|
}
|