From 82502d21c5ea4bbdecd458481bd75488cecd4e10 Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Mon, 27 May 2024 14:19:52 +0400 Subject: [PATCH] feat(repeater): support stringified enum value serialization closes #170 --- .../MessagePackStringEnumFormatter.cs | 50 ++++++++++++ src/SecTester.Repeater/Bus/IncomingRequest.cs | 13 +++- src/SecTester.Repeater/Protocol.cs | 3 + .../MessagePackHttpHeadersFormatterTests.cs | 69 +++++++++-------- .../MessagePackHttpMethodFormatterTests.cs | 32 ++++---- .../MessagePackStringEnumFormatterTests.cs | 76 +++++++++++++++++++ .../Bus/IncomingRequestTests.cs | 2 +- 7 files changed, 192 insertions(+), 53 deletions(-) create mode 100644 src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs create mode 100644 test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs diff --git a/src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs b/src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs new file mode 100644 index 0000000..76f9c94 --- /dev/null +++ b/src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using MessagePack; +using MessagePack.Formatters; + +namespace SecTester.Repeater.Bus.Formatters; + +// ADHOC: MessagePack-CSharp prohibits declaration of IMessagePackFormatter requesting to use System.Enum instead, refer to formatter interface argument type check +// https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/db2320b3338735c9266110bbbfffe63f17dfdf46/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Resolvers/DynamicObjectResolver.cs#L623 + +public class MessagePackStringEnumFormatter : IMessagePackFormatter + where T : Enum +{ + private static readonly Dictionary EnumToString = typeof(T) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Select(field => new + { + Value = (T)field.GetValue(null), + StringValue = field.GetCustomAttribute()?.Value ?? field.Name + }) + .ToDictionary(x => x.Value, x => x.StringValue); + + private static readonly Dictionary StringToEnum = EnumToString.ToDictionary(x => x.Value, x => x.Key); + + public void Serialize(ref MessagePackWriter writer, Enum value, MessagePackSerializerOptions options) + { + if (!EnumToString.TryGetValue((T)value, out var stringValue)) + { + throw new MessagePackSerializationException($"No string representation found for {value}"); + } + + writer.Write(stringValue); + } + + public Enum Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + + var stringValue = reader.ReadString(); + + if (!StringToEnum.TryGetValue(stringValue, out var enumValue)) + { + throw new MessagePackSerializationException($"Unable to parse '{stringValue}' to {typeof(T).Name}."); + } + + return enumValue; + } +} diff --git a/src/SecTester.Repeater/Bus/IncomingRequest.cs b/src/SecTester.Repeater/Bus/IncomingRequest.cs index b1e6fc6..1117e34 100644 --- a/src/SecTester.Repeater/Bus/IncomingRequest.cs +++ b/src/SecTester.Repeater/Bus/IncomingRequest.cs @@ -3,6 +3,8 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Reflection; +using System.Runtime.Serialization; using MessagePack; using SecTester.Repeater.Bus.Formatters; using SecTester.Repeater.Runners; @@ -15,6 +17,15 @@ public record IncomingRequest(Uri Url) : HttpMessage, IRequest private const string UrlKey = "url"; private const string MethodKey = "method"; + private static readonly Dictionary ProtocolEntries = typeof(Protocol) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Select(field => new + { + Value = (Protocol)field.GetValue(null), + StringValue = field.GetCustomAttribute()?.Value ?? field.Name + }) + .ToDictionary(x => x.StringValue, x => x.Value); + [Key(MethodKey)] [MessagePackFormatter(typeof(MessagePackHttpMethodFormatter))] public HttpMethod Method { get; set; } = HttpMethod.Get; @@ -24,7 +35,7 @@ public record IncomingRequest(Uri Url) : HttpMessage, IRequest public static IncomingRequest FromDictionary(Dictionary dictionary) { - var protocol = dictionary.TryGetValue(ProtocolKey, out var p) && p is string && Enum.TryParse(p.ToString(), out var e) + var protocol = dictionary.TryGetValue(ProtocolKey, out var p) && p is string && ProtocolEntries.TryGetValue(p.ToString(), out var e) ? e : Protocol.Http; diff --git a/src/SecTester.Repeater/Protocol.cs b/src/SecTester.Repeater/Protocol.cs index 4221d78..f35c880 100644 --- a/src/SecTester.Repeater/Protocol.cs +++ b/src/SecTester.Repeater/Protocol.cs @@ -1,6 +1,9 @@ +using System.Runtime.Serialization; + namespace SecTester.Repeater; public enum Protocol { + [EnumMember(Value = "http")] Http } diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs index 248f145..8be0cb1 100644 --- a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpHeadersFormatterTests.cs @@ -15,47 +15,52 @@ public record HttpHeadersDto private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; - private static IEnumerable Fixtures => new[] + public static readonly IEnumerable Fixtures = new List() { - new HttpHeadersDto + new object[] { - Headers = null + new HttpHeadersDto + { + Headers = null + } }, - new HttpHeadersDto + new object[] { - Headers = new List>>() + new HttpHeadersDto + { + Headers = new List>>() + } }, - new HttpHeadersDto + new object[] { - Headers = new List>> + new HttpHeadersDto { - new("content-type", new List { "application/json" }), - new("cache-control", new List { "no-cache", "no-store" }) + Headers = new List>> + { + new("content-type", new List { "application/json" }), + new("cache-control", new List { "no-cache", "no-store" }) + } } } }; - public static readonly IEnumerable WrongFormatFixtures = new[] + public static readonly IEnumerable WrongValueFixtures = new List() { - "{\"headers\":5}", - "{\"headers\":[]}", - "{\"headers\":{\"content-type\":{\"foo\"}:{\"bar\"}}}", - "{\"headers\":{\"content-type\":1}}", - "{\"headers\":{\"content-type\":[null]}}", - "{\"headers\":{\"content-type\":[1]}}" - }; - - public static IEnumerable SerializeDeserializeFixtures => Fixtures - .Select((x) => new object?[] + new object[] { - x, x - }); + "{\"headers\":5}" + }, + new object[] { "{\"headers\":[]}" }, + new object[] { "{\"headers\":{\"content-type\":{\"foo\"}:{\"bar\"}}}" }, + new object[] { "{\"headers\":{\"content-type\":1}}" }, + new object[] { "{\"headers\":{\"content-type\":[null]}}" }, + new object[] { "{\"headers\":{\"content-type\":[1]}}" } + }; [Theory] - [MemberData(nameof(SerializeDeserializeFixtures))] + [MemberData(nameof(Fixtures))] public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( - HttpHeadersDto input, - HttpHeadersDto expected) + HttpHeadersDto input) { // arrange var serialized = MessagePackSerializer.Serialize(input, Options); @@ -64,7 +69,7 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeseriali var result = MessagePackSerializer.Deserialize(serialized, Options); // assert - result.Should().BeEquivalentTo(expected); + result.Should().BeEquivalentTo(input); } [Fact] @@ -83,15 +88,9 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMis }); } - public static IEnumerable ThrowWhenWrongFormatFixtures => WrongFormatFixtures - .Select((x) => new object?[] - { - x - }); - [Theory] - [MemberData(nameof(ThrowWhenWrongFormatFixtures))] - public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrow( + [MemberData(nameof(WrongValueFixtures))] + public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrowWhenDataHasWrongValue( string input) { // arrange @@ -102,6 +101,6 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrow( // assert act.Should().Throw().WithMessage( - "Failed to deserialize SecTester.Repeater.Tests.Bus.Formatters.MessagePackHttpHeadersFormatterTests+HttpHeadersDto value."); + "Failed to deserialize*"); } } diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs index b4d30f9..356fcea 100644 --- a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackHttpMethodFormatterTests.cs @@ -15,35 +15,35 @@ public record HttpMethodDto private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; - private static IEnumerable - Fixtures => - new[] + public static readonly IEnumerable Fixtures = new List() + { + new object[] { new HttpMethodDto { Method = null - }, + } + }, + new object[] + { new HttpMethodDto { Method = HttpMethod.Get - }, + } + }, + new object[] + { new HttpMethodDto { Method = new HttpMethod("PROPFIND") } - }; - - public static IEnumerable SerializeDeserializeFixtures => Fixtures - .Select((x) => new object?[] - { - x, x - }); + } + }; [Theory] - [MemberData(nameof(SerializeDeserializeFixtures))] + [MemberData(nameof(Fixtures))] public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue( - HttpMethodDto input, - HttpMethodDto expected) + HttpMethodDto input) { // arrange var serialized = MessagePackSerializer.Serialize(input, Options); @@ -52,7 +52,7 @@ public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializ var result = MessagePackSerializer.Deserialize(serialized, Options); // assert - result.Should().BeEquivalentTo(expected); + result.Should().BeEquivalentTo(input); } [Fact] diff --git a/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs new file mode 100644 index 0000000..ce44db5 --- /dev/null +++ b/test/SecTester.Repeater.Tests/Bus/Formatters/MessagePackStringEnumFormatterTests.cs @@ -0,0 +1,76 @@ +using System.Runtime.Serialization; +using MessagePack; +using SecTester.Repeater.Bus.Formatters; + +namespace SecTester.Repeater.Tests.Bus.Formatters; + +public sealed class MessagePackStringEnumFormatterTests +{ + public enum Foo + { + [EnumMember(Value = "bar")] + Bar = 0, + [EnumMember(Value = "baz_cux")] + BazCux = 1 + } + + [MessagePackObject] + public record EnumDto + { + [Key("foo")] + [MessagePackFormatter(typeof(MessagePackStringEnumFormatter))] + public Enum Foo { get; set; } + } + + private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; + + public static IEnumerable + Fixtures => + new List + { + new object[] { "{\"foo\":\"bar\"}" }, + new object[] { "{\"foo\":\"baz_cux\"}" } + }; + + public static IEnumerable + WrongValueFixtures => + new List + { + new object[] { "{\"foo\": null}" }, + new object[] { "{\"foo\": 5}" }, + new object[] { "{\"foo\":\"BazCux\"}" } + }; + + + [Theory] + [MemberData(nameof(Fixtures))] + public void MessagePackStringEnumFormatter_Serialize_ShouldCorrectlySerialize( + string input) + { + // arrange + var binary = MessagePackSerializer.ConvertFromJson(input, Options); + var obj = MessagePackSerializer.Deserialize(binary, Options); + + + // act + var result = MessagePackSerializer.SerializeToJson(obj, Options); + + // assert + result.Should().BeEquivalentTo(input); + } + + [Theory] + [MemberData(nameof(WrongValueFixtures))] + public void MessagePackStringEnumFormatter_Deserialize_ShouldThrowWhenDataHasWrongValue(string input) + { + // arrange + var binary = MessagePackSerializer.ConvertFromJson(input, Options); + + // act + var act = () => MessagePackSerializer.Deserialize(binary, Options); + + // assert + act.Should().Throw().WithMessage( + "Failed to deserialize*"); + } +} diff --git a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs index 0b91bbe..69e889f 100644 --- a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs @@ -57,7 +57,7 @@ public void IncomingRequest_FromDictionary_ShouldThrowWhenRequiredPropertyWasNot { // arrange var packJson = - "{\"type\":2,\"data\":[\"request\",{\"protocol\":0,\"headers\":{\"content-type\":\"application/json\",\"cache-control\":[\"no-cache\",\"no-store\"]},\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"method\":\"PROPFIND\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; + "{\"type\":2,\"data\":[\"request\",{\"protocol\":\"http:\",\"headers\":{\"content-type\":\"application/json\",\"cache-control\":[\"no-cache\",\"no-store\"]},\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"method\":\"PROPFIND\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard);