301 lines
12 KiB
C#
301 lines
12 KiB
C#
// Copyright 2021 Cloud Native Foundation.
|
|
// Licensed under the Apache 2.0 license.
|
|
// See LICENSE file in the project root for full license information.
|
|
|
|
using System;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using Xunit;
|
|
|
|
namespace CloudNative.CloudEvents.UnitTests
|
|
{
|
|
public class CloudEventAttributeTypeTest
|
|
{
|
|
public static readonly TheoryData<CloudEventAttributeType> AllTypes = new TheoryData<CloudEventAttributeType>
|
|
{
|
|
CloudEventAttributeType.Binary,
|
|
CloudEventAttributeType.Boolean,
|
|
CloudEventAttributeType.Integer,
|
|
CloudEventAttributeType.String,
|
|
CloudEventAttributeType.Timestamp,
|
|
CloudEventAttributeType.Uri,
|
|
CloudEventAttributeType.UriReference
|
|
};
|
|
|
|
[Fact]
|
|
public void Names()
|
|
{
|
|
Assert.Equal("Binary", CloudEventAttributeType.Binary.Name);
|
|
Assert.Equal("Boolean", CloudEventAttributeType.Boolean.Name);
|
|
Assert.Equal("Integer", CloudEventAttributeType.Integer.Name);
|
|
Assert.Equal("String", CloudEventAttributeType.String.Name);
|
|
Assert.Equal("Timestamp", CloudEventAttributeType.Timestamp.Name);
|
|
Assert.Equal("URI", CloudEventAttributeType.Uri.Name);
|
|
Assert.Equal("URI-Reference", CloudEventAttributeType.UriReference.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void OrdinalTypeNameMatchesPropertyName()
|
|
{
|
|
var properties = typeof(CloudEventAttributeType)
|
|
.GetProperties(BindingFlags.Public | BindingFlags.Static)
|
|
.Where(prop => prop.PropertyType == typeof(CloudEventAttributeType));
|
|
foreach (var property in properties)
|
|
{
|
|
var type = (CloudEventAttributeType) property.GetValue(null)!;
|
|
Assert.Equal(property.Name, type!.Ordinal.ToString());
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(AllTypes))]
|
|
public void ParseNull(CloudEventAttributeType type) =>
|
|
Assert.Throws<ArgumentNullException>(() => type.Parse(null!));
|
|
|
|
[Theory]
|
|
[MemberData(nameof(AllTypes))]
|
|
public void FormatNull(CloudEventAttributeType type) =>
|
|
Assert.Throws<ArgumentNullException>(() => type.Format(null!));
|
|
|
|
// None of our types can be constructed with a StringBuilder.
|
|
[Theory]
|
|
[MemberData(nameof(AllTypes))]
|
|
public void FormatIncorrectType(CloudEventAttributeType type) =>
|
|
Assert.Throws<ArgumentException>(() => type.Format(new StringBuilder()));
|
|
|
|
[Theory]
|
|
[MemberData(nameof(AllTypes))]
|
|
public void ValidateIncorrectType(CloudEventAttributeType type) =>
|
|
Assert.Throws<ArgumentException>(() => type.Validate(new StringBuilder()));
|
|
|
|
public class BinaryTypeTest
|
|
{
|
|
[Theory]
|
|
[InlineData("")]
|
|
// Examples from https://en.wikipedia.org/wiki/Base64
|
|
[InlineData("TWFu", (byte) 77, (byte) 97, (byte) 110)]
|
|
[InlineData("TWE=", (byte) 77, (byte) 97)]
|
|
[InlineData("TQ==", (byte) 77)]
|
|
public void ParseAndFormat_Valid(string text, params byte[] bytes)
|
|
{
|
|
var parsedBytes = CloudEventAttributeType.Binary.Parse(text);
|
|
// Convert both to hex to provide simpler comparisons.
|
|
Assert.Equal(Convert.ToString(bytes), Convert.ToString(parsedBytes));
|
|
|
|
var formattedBytes = CloudEventAttributeType.Binary.Format(bytes);
|
|
Assert.Equal(text, formattedBytes);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("x")]
|
|
[InlineData("TWFU=")]
|
|
[InlineData("==TQ")]
|
|
public void Parse_Invalid(string text)
|
|
{
|
|
Assert.Throws<FormatException>(() => CloudEventAttributeType.Binary.Parse(text));
|
|
}
|
|
}
|
|
|
|
public class BooleanTypeTest
|
|
{
|
|
[Theory]
|
|
[InlineData("false", false)]
|
|
[InlineData("true", true)]
|
|
public void ParseAndFormat_Valid(string text, bool value)
|
|
{
|
|
var parsedValue = (bool) CloudEventAttributeType.Boolean.Parse(text);
|
|
// Convert both to hex to provide simpler comparisons.
|
|
Assert.Equal(value, parsedValue);
|
|
|
|
var formattedValue = CloudEventAttributeType.Boolean.Format(value);
|
|
Assert.Equal(text, formattedValue);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData("TRUE")]
|
|
[InlineData("FALSE")]
|
|
[InlineData("maybe")]
|
|
public void Parse_Invalid(string text)
|
|
{
|
|
Assert.Throws<ArgumentException>(() => CloudEventAttributeType.Boolean.Parse(text));
|
|
}
|
|
}
|
|
|
|
public class IntegerTypeTest
|
|
{
|
|
[Theory]
|
|
[InlineData("-2147483648", -2147483648)]
|
|
[InlineData("-1", -1)]
|
|
[InlineData("0", 0)]
|
|
[InlineData("1", 1)]
|
|
[InlineData("2147483647", 2147483647)]
|
|
public void ParseAndFormat_Valid(string text, int value)
|
|
{
|
|
var parsedValue = (int) CloudEventAttributeType.Integer.Parse(text);
|
|
// Convert both to hex to provide simpler comparisons.
|
|
Assert.Equal(value, parsedValue);
|
|
|
|
var formattedValue = CloudEventAttributeType.Integer.Format(value);
|
|
Assert.Equal(text, formattedValue);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData("2147483648")] // Above int.MaxValue
|
|
[InlineData("-2147483649")] // Below int.MinValue
|
|
[InlineData("not an integer")]
|
|
[InlineData("1,000")]
|
|
[InlineData("1.5")]
|
|
[InlineData(" 10")] // Leading space
|
|
[InlineData("+10")] // Plus sign
|
|
[InlineData("10 ")] // Trailing space
|
|
public void Parse_Invalid(string text)
|
|
{
|
|
// Sometimes OverflowException, sometimes FormatException.
|
|
Assert.ThrowsAny<Exception>(() => CloudEventAttributeType.Integer.Parse(text));
|
|
}
|
|
}
|
|
|
|
public class StringTypeTest
|
|
{
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData("test")]
|
|
[InlineData("TEST")]
|
|
[InlineData("\U0001F600")]
|
|
[InlineData("x\U0001F600y")]
|
|
[InlineData("x\U0001F600y\U0001F600z")]
|
|
public void ParseAndFormat_Valid(string text)
|
|
{
|
|
var parseResult = (string) CloudEventAttributeType.String.Parse(text);
|
|
Assert.Equal(parseResult, text);
|
|
var formatResult = CloudEventAttributeType.String.Format(text);
|
|
Assert.Equal(text, formatResult);
|
|
CloudEventAttributeType.String.Validate(text);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("\n")] // Control character (first range)
|
|
[InlineData("\u007f")] // Control character (second range)
|
|
[InlineData("\ufdd0")] // Non-character (first range)
|
|
[InlineData("\ufffe")] // Non-character (second range)
|
|
[InlineData("\U0001FFFE")] // Non-character (surrogate range)
|
|
[InlineData("\U0010FFFE")] // Non-character (surrogate range)
|
|
public void InvalidCharacters(string text)
|
|
{
|
|
Assert.Throws<ArgumentException>(() => CloudEventAttributeType.String.Validate(text));
|
|
}
|
|
|
|
// Note: these are specified separately as .NET string attributes are stored as UTF-8
|
|
// internally, which means you can't express invalid strings in attributes (and get them
|
|
// out again).
|
|
[Theory]
|
|
[InlineData(0xd800, 0x20)] // High surrogate followed by non-surrogate
|
|
[InlineData(0xdc00, 0x20)] // Low surrogate at start of string
|
|
[InlineData(0x20, 0xdc00)] // Non-surrogate followed by low surrogate
|
|
[InlineData(0xd800, 0xd800)] // High surrogate followed by high surrogate
|
|
[InlineData(0x20, 0xd800)] // High surrogate at end of string
|
|
public void InvalidSurrogates(int first, int second)
|
|
{
|
|
string text = $"{(char) first}{(char) second}";
|
|
Assert.Throws<ArgumentException>(() => CloudEventAttributeType.String.Validate(text));
|
|
}
|
|
}
|
|
|
|
public class TimestampTest
|
|
{
|
|
// Note: this is not particularly exhaustive, as we have a more comprehensive set of tests
|
|
// in TimestampsTest.
|
|
|
|
[Theory]
|
|
[InlineData("2021-01-18T14:52:01Z")]
|
|
[InlineData("2000-02-29T01:23:45.678+01:30")]
|
|
[InlineData("2000-02-29T01:23:45-01:30")]
|
|
public void ParseAndFormat_Valid(string text)
|
|
{
|
|
var parsed = CloudEventAttributeType.Timestamp.Parse(text);
|
|
var formatted = CloudEventAttributeType.Timestamp.Format(parsed);
|
|
Assert.Equal(text, formatted);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("2021-01-18T14:52:01")] // No UTC offset indicator
|
|
[InlineData("2021-01-18T14:52:01.1234567XYZ")] // Garbage after 7 significant digits of sub-second
|
|
[InlineData("20garbage2T14:52:01Z01")] // Text after UTC offset indicator
|
|
[InlineData("2021-01-18T14:52:01X")] // Garbage UTC offset indicator
|
|
public void Parse_Invalid(string text)
|
|
{
|
|
Assert.Throws<FormatException>(() => CloudEventAttributeType.Timestamp.Parse(text));
|
|
}
|
|
}
|
|
|
|
public class UriTest
|
|
{
|
|
[Theory]
|
|
[InlineData("https://cloudevents.io/")]
|
|
[InlineData("https://cloudevents.io/path?query=value")]
|
|
[InlineData("ftp://cloudevents.io/path")]
|
|
[InlineData("ftp://cloudevents.io/path#fragment")]
|
|
// It's unclear why System.Uri thinks this doesn't need escaping, but apparently
|
|
// it doesn't (so we now just return the original string in all cases)
|
|
[InlineData("http://host/\u00a3")]
|
|
// These would not round-trip if we just called ToString.
|
|
[InlineData("https://cloudevents.io")]
|
|
[InlineData("https://cloudevents.io?query=value")]
|
|
public void ParseAndFormat_Roundtrip(string text)
|
|
{
|
|
var parsed = CloudEventAttributeType.Uri.Parse(text);
|
|
CloudEventAttributeType.Uri.Validate(parsed);
|
|
Assert.Equal(new Uri(text), parsed);
|
|
var formatted = CloudEventAttributeType.Uri.Format(parsed);
|
|
Assert.Equal(text, formatted);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("//cloudevents.io?query=value")]
|
|
[InlineData("/path-absolute")]
|
|
[InlineData("")]
|
|
public void Parse_Invalid(string text)
|
|
{
|
|
Assert.Throws<UriFormatException>(() => CloudEventAttributeType.Uri.Parse(text));
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Invalid()
|
|
{
|
|
var uri = new Uri("//relative", UriKind.Relative);
|
|
Assert.Throws<ArgumentException>(() => CloudEventAttributeType.Uri.Validate(uri));
|
|
}
|
|
}
|
|
|
|
public class UriReferenceTest
|
|
{
|
|
[Theory]
|
|
[InlineData("https://cloudevents.io/?query=value")]
|
|
[InlineData("ftp://cloudevents.io/path")]
|
|
[InlineData("")] // Empty strings are valid URI references
|
|
[InlineData("//authority/path")]
|
|
[InlineData("/path-absolute")]
|
|
[InlineData("path-noscheme")]
|
|
[InlineData("#fragment")]
|
|
// These three really shouldn't round-trip. They're not valid URIs as they are,
|
|
// but it's hard to prevent that without also rejecting "#fragment", due to Uri's
|
|
// behavior. I'd expect the Uri constructor to automatically escape the leading
|
|
// character, but apparently it doesn't.
|
|
[InlineData(":colon-start")]
|
|
[InlineData("[open-bracket-start")]
|
|
[InlineData("]close-bracket-start")]
|
|
public void ParseFormatValidate_Valid(string text)
|
|
{
|
|
var parsed = CloudEventAttributeType.UriReference.Parse(text);
|
|
CloudEventAttributeType.UriReference.Validate(parsed);
|
|
Assert.Equal(new Uri(text, UriKind.RelativeOrAbsolute), parsed);
|
|
var formatted = CloudEventAttributeType.UriReference.Format(parsed);
|
|
Assert.Equal(text, formatted);
|
|
}
|
|
}
|
|
}
|
|
}
|